diff --git a/Infosec/.dockerignore b/.dockerignore similarity index 100% rename from Infosec/.dockerignore rename to .dockerignore diff --git a/Infosec/.env.example b/.env.example similarity index 100% rename from Infosec/.env.example rename to .env.example diff --git a/Infosec/.gitignore b/.gitignore similarity index 100% rename from Infosec/.gitignore rename to .gitignore diff --git a/Alert Hub System/.dockerignore b/Alert Hub System/.dockerignore deleted file mode 100644 index 378ea1e..0000000 --- a/Alert Hub System/.dockerignore +++ /dev/null @@ -1,19 +0,0 @@ -.git -.gitignore -.env -*.md -__pycache__/ -*.pyc -*.pyo -*.pyd -.venv/ -venv/ -.env.local -data/ -*.db -*.sqlite* -.DS_Store -.idea/ -.vscode/ -node_modules/ -tests/ diff --git a/Alert Hub System/.env.example b/Alert Hub System/.env.example deleted file mode 100644 index 699da0c..0000000 --- a/Alert Hub System/.env.example +++ /dev/null @@ -1,79 +0,0 @@ -# ============================================================================= -# AlertHub Configuration -# All configuration is done here. NEVER commit your real .env to source control. -# ============================================================================= - -# ---- Flask Core ---- -# Generate a strong key: python -c "import secrets; print(secrets.token_hex(32))" -SECRET_KEY=change-me-to-a-long-random-string-please-generate-with-secrets-module -FLASK_ENV=production - -# ---- Ports ---- -# AlertHub now runs as a SINGLE service on one port. The admin backoffice is -# served on the same port under the /backoffice path (admins only). -FRONTOFFICE_PORT=8080 - -# BACKOFFICE_PORT is no longer used (the backoffice shares the main port under -# /backoffice). It is kept here only so old .env files remain valid; you can -# safely delete it. -# BACKOFFICE_PORT=9001 - -# ---- Gunicorn (optional) ---- -# Keep GUNICORN_WORKERS at 1 so the reminder scheduler runs exactly once. -# Increase GUNICORN_THREADS for more concurrency instead. -GUNICORN_WORKERS=1 -GUNICORN_THREADS=4 - -# ---- Database ---- -DATABASE_PATH=/data/alerthub.db - -# ---- Initial Administrator ---- -# There is NO seeded admin any more. On first launch, open the app and you'll be -# taken to a one-time "Create administrator" screen to set the username & -# password yourself. (The variables below are legacy/no-ops, kept for reference.) -# INITIAL_ADMIN_USERNAME=admin -# INITIAL_ADMIN_EMAIL=admin@example.com -# INITIAL_ADMIN_PASSWORD= - -# ---- Session / Security ---- -# Set SESSION_COOKIE_SECURE=true if you serve via HTTPS (recommended in production) -SESSION_COOKIE_SECURE=false -SESSION_LIFETIME_HOURS=8 -PASSWORD_MIN_LENGTH=10 - -# ---- MFA ---- -# MFA is optional and self-service: any user can enable it from Account → MFA. -MFA_ISSUER_NAME=AlertHub - -# ---- Scheduler ---- -# How often (in minutes) the scheduler checks alerts and dispatches reminder emails -SCHEDULER_INTERVAL_MINUTES=60 - -# ---- Default Reminder Lead Times (in days) ---- -# Comma-separated list. Configurable per-alert later, but these are defaults. -DEFAULT_REMINDER_DAYS=30,14,7,1 - -# ---- SMTP / Outgoing mail (Purelymail defaults) ---- -# Note: these values seed the DB on first run only. After that, edit them in -# the backoffice at /mail-config. -# Purelymail SMTP: smtp.purelymail.com : 465 (SSL/TLS) -# If your client only supports STARTTLS, use port 587 with SMTP_ENCRYPTION=starttls -SMTP_HOST=smtp.purelymail.com -SMTP_PORT=465 -SMTP_USERNAME=you@yourdomain.com -SMTP_PASSWORD= -SMTP_ENCRYPTION=ssl -# Allowed: none | starttls | ssl -SMTP_FROM_ADDRESS=you@yourdomain.com -SMTP_FROM_NAME=AlertHub Notifications - -# ---- IMAP / Sent-folder copy (Purelymail defaults) ---- -# When a mail is sent from the platform, a copy is appended to the mailbox's -# Sent folder over IMAP so it shows up in your normal mail client. -# Purelymail IMAP: imap.purelymail.com : 993 (SSL/TLS) -# Leave IMAP_USERNAME / IMAP_PASSWORD blank to reuse the SMTP credentials. -IMAP_HOST=imap.purelymail.com -IMAP_PORT=993 -IMAP_USERNAME= -IMAP_PASSWORD= -IMAP_SENT_FOLDER=Sent diff --git a/Alert Hub System/.gitignore b/Alert Hub System/.gitignore deleted file mode 100644 index 9bc2f00..0000000 --- a/Alert Hub System/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.env -*.pyc -__pycache__/ -.venv/ -venv/ -data/ -*.db -*.sqlite* -.DS_Store -.idea/ -.vscode/ diff --git a/Alert Hub System/Dockerfile b/Alert Hub System/Dockerfile deleted file mode 100644 index dedbe96..0000000 --- a/Alert Hub System/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# syntax=docker/dockerfile:1.7 -# ---- Build / runtime image ------------------------------------------------- -# python:3.13-slim-trixie is the current stable Debian-based slim variant. -FROM python:3.13-slim-trixie - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 - -# Install only what we strictly need (curl for healthchecks). -# Use --no-install-recommends, then immediately purge apt lists to keep -# the image small. -RUN apt-get update \ - && apt-get install -y --no-install-recommends curl \ - && rm -rf /var/lib/apt/lists/* - -# Create an unprivileged user -RUN groupadd --system --gid 10001 alerthub \ - && useradd --system --uid 10001 --gid alerthub --home-dir /app --shell /usr/sbin/nologin alerthub - -WORKDIR /app - -# Install Python dependencies first (better layer cache) -COPY --chown=alerthub:alerthub requirements.txt ./ -RUN pip install --upgrade pip \ - && pip install --no-cache-dir -r requirements.txt - -# Copy application sources -COPY --chown=alerthub:alerthub app/ ./app/ -COPY --chown=alerthub:alerthub wsgi.py wsgi_frontoffice.py wsgi_backoffice.py entrypoint.sh ./ - -RUN chmod +x entrypoint.sh \ - && mkdir -p /data \ - && chown -R alerthub:alerthub /app /data - -USER alerthub - -EXPOSE 8080 - -# Start the unified app (frontoffice + /backoffice) on a single port. -ENTRYPOINT ["./entrypoint.sh"] - -HEALTHCHECK --interval=30s --timeout=4s --start-period=10s --retries=3 \ - CMD curl -fsS http://127.0.0.1:${FRONTOFFICE_PORT:-8080}/login >/dev/null || exit 1 diff --git a/Alert Hub System/OLD_VERSIONS/v1.0/alerthub.zip b/Alert Hub System/OLD_VERSIONS/v1.0/alerthub.zip deleted file mode 100644 index a8503c0..0000000 Binary files a/Alert Hub System/OLD_VERSIONS/v1.0/alerthub.zip and /dev/null differ diff --git a/Alert Hub System/OLD_VERSIONS/v1.2/alerthub.zip b/Alert Hub System/OLD_VERSIONS/v1.2/alerthub.zip deleted file mode 100644 index 5881d11..0000000 Binary files a/Alert Hub System/OLD_VERSIONS/v1.2/alerthub.zip and /dev/null differ diff --git a/Alert Hub System/OLD_VERSIONS/v1.3/alerthub.zip b/Alert Hub System/OLD_VERSIONS/v1.3/alerthub.zip deleted file mode 100644 index 2fef457..0000000 Binary files a/Alert Hub System/OLD_VERSIONS/v1.3/alerthub.zip and /dev/null differ diff --git a/Alert Hub System/OLD_VERSIONS/v1.4/alerthub.zip b/Alert Hub System/OLD_VERSIONS/v1.4/alerthub.zip deleted file mode 100644 index 18dd74c..0000000 Binary files a/Alert Hub System/OLD_VERSIONS/v1.4/alerthub.zip and /dev/null differ diff --git a/Alert Hub System/OLD_VERSIONS/v1.5/alerthub.zip b/Alert Hub System/OLD_VERSIONS/v1.5/alerthub.zip deleted file mode 100644 index 99d9d11..0000000 Binary files a/Alert Hub System/OLD_VERSIONS/v1.5/alerthub.zip and /dev/null differ diff --git a/Alert Hub System/README.md b/Alert Hub System/README.md deleted file mode 100644 index a779da7..0000000 --- a/Alert Hub System/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# AlertHub - -**Version 1.5 — Martinhal IT (Joao Vaz)** - -A Dockerized alert/reminder web application with email notifications, MFA-capable authentication, and role-based access control. Designed for tracking expiration dates (licenses, renewals, certificates, contracts) with live countdown timers and configurable email reminders. - -## Features - -- **First-run admin setup** — On the very first launch (no accounts yet) you're taken to a "Create administrator" screen to set the admin username & password yourself. No seeded/default admin credentials. -- **Single-port architecture** — Everything runs on one port (`:8080`). The frontoffice (alerts + management) is at the root; the admin backoffice (users, mail, settings, audit log, mail log) is served on the **same port** under `/backoffice` and is restricted to admins. -- **Backoffice shortcut** — Admins get a **Backoffice** link in the top bar (and a **Frontoffice** link back) that switches between the two areas. Only visible to admins. -- **Authentication** — Password (bcrypt, 12 rounds) with optional TOTP MFA. **Any user** can self-enable MFA from *Account → MFA*; it is never mandatory. -- **User avatars** — Any user can upload a profile picture (PNG/JPG/GIF/WebP). Images are auto-squared and resized to 256×256 and stored in the database (survives upgrades). Shown in the top bar. -- **RBAC** — Two roles: `user` (view-only) and `admin` (full access) -- **Live countdown timers** — Per-alert ticking timers (days/hours/minutes/seconds) with state-based color coding (warning, critical, expired) -- **Email reminders** — Configurable SMTP + IMAP editable from the backoffice. Per-alert override of reminder lead times. -- **Mail log** — Every message the platform sends is recorded in the backoffice **Mail Log** (recipients, subject, delivery status, whether the Sent-folder copy was saved, and who triggered it). -- **Sent-folder copy (IMAP)** — After each successful send, a copy of the message is appended to the mailbox's **Sent** folder over IMAP, so it appears in your normal mail client. -- **Log export & email** — Both the Audit log and Mail log can be exported to CSV or emailed (as a CSV attachment) to any destination address. -- **Account security** — Lockout after 5 failed attempts (15-minute window), audit log of sensitive actions, security headers (CSP, X-Frame-Options, HSTS-ready) -- **IT-terminal aesthetic** — JetBrains Mono + IBM Plex Sans, signal-green accents, light + dark mode (auto-detect + manual toggle) -- **Consistent footer** — `© 2026 Martinhal IT - Joao Vaz - Version 1.5` on every page except the login screen. -- **Hardened container** — Non-root user, all capabilities dropped, `no-new-privileges`, healthcheck - -## Stack - -Python 3.13 · Flask 3.1 · SQLAlchemy 2 · Flask-Login · Flask-WTF · APScheduler · pyotp · bcrypt · gunicorn · SQLite - -## Quick start - -```bash -# 1. Get the code, then: -cp .env.example .env - -# 2. Edit .env — at minimum, change: -# - SECRET_KEY (use: python -c "import secrets; print(secrets.token_hex(32))") -# - SMTP_* / IMAP_* (your Purelymail credentials) - -# 3. Build and start: -docker compose up -d --build - -# 4. Open: -# App: http://localhost:8080 -# Backoffice: http://localhost:8080/backoffice (admins only) - -# 5. First visit → you'll be sent to the "Create administrator" screen. -# Set the admin username, email, and password there. -``` - -### First-run checklist - -1. Open the app — you'll land on **Create administrator**. Set the admin username, email and password. -2. Sign in with the credentials you just created. -3. (Optional) Go to **Account** → **Set up MFA** (scan the QR code with Google Authenticator, Authy, 1Password, etc.). Any user can do this. -4. (Optional) **Account** → **Profile avatar** → upload a picture. -5. Open the backoffice → **Mail** → confirm the Purelymail SMTP/IMAP settings, fill in the mailbox username/password, and send a test email. Check the **Mail Log** to confirm it sent and that a copy was saved to the mailbox's Sent folder. -6. Backoffice → **Settings** → set the default reminder lead times (e.g. `30,14,7,1` days before expiry). -7. Frontoffice → **Manage Alerts** → add your first alert. - -## Configuration - -All configuration lives in `.env`. The `docker-compose.yml` only references the file via `env_file` — no secrets are baked into the image or compose file. - -| Variable | Purpose | -|---|---| -| `SECRET_KEY` | Flask session signing key (required, generate a new one) | -| `FRONTOFFICE_PORT` | The single host port (default 8080). `BACKOFFICE_PORT` is no longer used. | -| `GUNICORN_WORKERS` / `GUNICORN_THREADS` | Web server sizing. Keep workers at 1 so the reminder scheduler runs once (default 1 worker / 4 threads). | -| `DATABASE_PATH` | SQLite path inside the container (default `/data/alerthub.db`) | -| `SESSION_LIFETIME_HOURS` | Login session length | -| `MAX_LOGIN_ATTEMPTS` / `LOCKOUT_MINUTES` | Brute-force protection | -| `SCHEDULER_INTERVAL_MINUTES` | How often the reminder job runs (default 60) | -| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USERNAME` / `SMTP_PASSWORD` / `SMTP_ENCRYPTION` / `SMTP_FROM_ADDRESS` / `SMTP_FROM_NAME` | Initial outgoing-mail config (Purelymail defaults; seeded into DB on first run; admins edit live values via backoffice afterwards) | -| `IMAP_HOST` / `IMAP_PORT` / `IMAP_USERNAME` / `IMAP_PASSWORD` / `IMAP_SENT_FOLDER` | IMAP settings for saving a copy of sent mail to the Sent folder (Purelymail defaults). Leave user/pass blank to reuse the SMTP credentials. | -| `DEFAULT_REMINDER_DAYS` | Default reminder thresholds, comma-separated (e.g. `30,14,7,1`) | - -> **Admin account:** There is no seeded admin. The first launch presents a one-time **Create administrator** screen. The old `INITIAL_ADMIN_*` variables are no longer used. - -> **Note:** `SMTP_*` / `IMAP_*` values seed the database on first launch. After that, the backoffice **Mail** page is the source of truth — edits there persist independently of `.env`. - -### Purelymail mail settings - -Defaults ship pre-configured for Purelymail: - -- **Outgoing (SMTP):** `smtp.purelymail.com`, port **465**, SSL/TLS. (If your client only supports STARTTLS, use port **587** with `SMTP_ENCRYPTION=starttls`.) -- **Incoming (IMAP):** `imap.purelymail.com`, port **993**, SSL/TLS — used to append sent messages to the **Sent** folder. - -Just fill in your mailbox username and password (in `.env` or the backoffice **Mail** page). - -## Upgrading in place (no data loss) - -This build is designed to be **unzipped over the previous version**. On startup the app runs additive migrations: it creates any new tables (e.g. the mail log) and adds any new columns (avatars) to the existing SQLite database in the `/data` volume, without touching your existing users, alerts, settings, or audit history. - -```bash -# from the folder containing your existing docker-compose.yml / .env / app: -unzip -o alerthub.zip # overwrite code in place (keeps .env and the /data volume) -docker compose up -d --build # rebuild & restart; migrations run automatically -``` - -Your customised SMTP settings are preserved on upgrade; the new IMAP keys are seeded with the Purelymail defaults (adjust them in the backoffice **Mail** page). - -## Architecture - -``` -┌──────────── Docker container ────────────┐ -│ │ -│ entrypoint.sh │ -│ └── gunicorn wsgi:application :8080 │ -│ ├── frontoffice (/) │ -│ ├── backoffice (/backoffice) │ -│ └── scheduler (APScheduler) │ -│ │ -│ /data/alerthub.db (named volume) │ -└──────────────────────────────────────────┘ -``` - -Both processes share the same SQLite database (mounted as a Docker named volume). The reminder scheduler runs only inside the frontoffice process to avoid duplicate emails. - -## Security notes - -- Passwords stored as bcrypt hashes (cost 12). -- TOTP secrets stored encrypted at rest is NOT implemented — the secret is stored as-is. If your threat model requires it, mount the DB on an encrypted volume. -- CSP is strict (no inline scripts except the early theme bootstrap which is `nonce`-able if you tighten further). -- Container runs as uid `10001`, drops all Linux capabilities, and uses `no-new-privileges`. -- Brute-force lockout is per-username, not per-IP. - -## Operational tips - -- **Trigger a reminder check manually:** Backoffice → Settings → "Run check now". -- **Reset a user's MFA:** Backoffice → Users → row menu → Reset MFA. -- **Unlock a locked-out user:** Backoffice → Users → row menu → Unlock. -- **Inspect activity:** Backoffice → Audit log (paginated). -- **Reset reminder history for an alert** (e.g. after extending the date): Manage Alerts → row → Reset reminders. - -## Uninstall / data reset - -```bash -docker compose down # stop containers, keep data -docker compose down -v # also remove the alerthub_data volume (deletes DB) -``` - -## License - -Provided as-is for internal use. No warranty. diff --git a/Alert Hub System/app/__init__.py b/Alert Hub System/app/__init__.py deleted file mode 100644 index 46a18fb..0000000 --- a/Alert Hub System/app/__init__.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Application factory. - -Builds a single unified Flask app served on ONE port: - - frontoffice at the root: / (View) and /alerts (Add/Edit/Remove) - - backoffice under /backoffice: users, mail config, settings, audit log, - mail log — all restricted to admins. - -Everything shares the same database, models, and login system. The ``variant`` -argument is retained for backwards compatibility with existing WSGI entry -points but no longer changes what is registered — both areas are always -available on the same app. -""" -import os -import logging -from flask import Flask, redirect, url_for, render_template, request, session -from flask_login import LoginManager -from flask_wtf.csrf import CSRFProtect -from sqlalchemy import inspect, text -from sqlalchemy.exc import IntegrityError, OperationalError - -from .config import Config -from .models import db, User, AppSetting -from .scheduler import init_scheduler - -APP_VERSION = "1.5" - -login_manager = LoginManager() -csrf = CSRFProtect() - - -def _setup_logging(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - ) - - -def _run_migrations(app: Flask): - """Lightweight, additive schema migrations so an existing DB can be - upgraded in place (unzip-over-the-top) without any data loss. - - Only ADDs missing tables/columns — never drops or alters existing data. - Every step is idempotent and tolerant of a concurrent process having - already applied it (both gunicorn apps boot against the same file). - """ - with app.app_context(): - # 1. Create any brand-new tables (e.g. mail_log) and missing tables. - try: - db.create_all() - except OperationalError as e: - # Another booting process created a table at the same moment. - app.logger.info("create_all raced (ignored): %s", e) - db.session.rollback() - - # 2. Add any missing columns on the users table. - inspector = inspect(db.engine) - existing_cols = {c["name"] for c in inspector.get_columns("users")} - add_column = { - "avatar_data": "ALTER TABLE users ADD COLUMN avatar_data BLOB", - "avatar_mime": "ALTER TABLE users ADD COLUMN avatar_mime VARCHAR(64)", - } - for col, ddl in add_column.items(): - if col in existing_cols: - continue - try: - with db.engine.begin() as conn: - conn.execute(text(ddl)) - app.logger.info("Migration: added users.%s", col) - except OperationalError as e: - # "duplicate column name" — another process won the race. Fine. - app.logger.info("Add column %s raced (ignored): %s", col, e) - - -def _seed_settings(app: Flask): - """Seed default settings (mail config, IMAP, reminders) when rows are - missing. Never overwrites values an admin has already set — so upgrades - keep existing configuration. New keys (e.g. IMAP) are seeded on upgrade. - - Uses an INSERT that tolerates a concurrent process seeding the same key. - """ - with app.app_context(): - defaults = { - # ---- SMTP (outgoing) — Purelymail defaults ---- - "smtp_host": app.config.get("SMTP_HOST", "smtp.purelymail.com"), - "smtp_port": str(app.config.get("SMTP_PORT", 465)), - "smtp_username": app.config.get("SMTP_USERNAME", ""), - "smtp_password": app.config.get("SMTP_PASSWORD", ""), - "smtp_encryption": app.config.get("SMTP_ENCRYPTION", "ssl"), - "smtp_from_address": app.config.get("SMTP_FROM_ADDRESS", ""), - "smtp_from_name": app.config.get("SMTP_FROM_NAME", "AlertHub"), - # ---- IMAP (Sent-folder copy) — Purelymail defaults ---- - "imap_enabled": "true", - "imap_host": app.config.get("IMAP_HOST", "imap.purelymail.com"), - "imap_port": str(app.config.get("IMAP_PORT", 993)), - "imap_username": app.config.get("IMAP_USERNAME", ""), - "imap_password": app.config.get("IMAP_PASSWORD", ""), - "imap_sent_folder": app.config.get("IMAP_SENT_FOLDER", "Sent"), - # ---- Reminders ---- - "default_reminder_days": app.config.get("DEFAULT_REMINDER_DAYS", "30,14,7,1"), - } - for k, v in defaults.items(): - if AppSetting.get(k) is not None: - continue - try: - row = AppSetting(key=k, value=v) - db.session.add(row) - db.session.commit() - except IntegrityError: - # A concurrent boot inserted this key first — that's fine. - db.session.rollback() - - -def _initialise_database(app: Flask): - """Run migrations + seeding exactly once at a time across processes. - - The container starts several worker processes that all import the app and - hit the shared SQLite file simultaneously. A cross-process file lock - serialises initialisation so concurrent DDL/seeding can't collide. - """ - import fcntl # POSIX-only; the container is Linux. - - db_dir = os.path.dirname(app.config["DATABASE_PATH"]) or "." - os.makedirs(db_dir, exist_ok=True) - lock_path = os.path.join(db_dir, ".alerthub-init.lock") - - try: - lock_file = open(lock_path, "w") - except OSError: - # If we somehow can't open the lock file, fall back to unlocked init; - # the per-step tolerance above still guards against most races. - _run_migrations(app) - _seed_settings(app) - return - - with lock_file: - try: - fcntl.flock(lock_file, fcntl.LOCK_EX) - except OSError: - pass # advisory lock unavailable — proceed; steps are tolerant. - try: - _run_migrations(app) - _seed_settings(app) - finally: - try: - fcntl.flock(lock_file, fcntl.LOCK_UN) - except OSError: - pass - - -def create_app(variant: str = "frontoffice") -> Flask: - """Build the unified Flask app (frontoffice + /backoffice on one port). - - ``variant`` is accepted for backwards compatibility with the existing WSGI - entry points but no longer restricts which blueprints are registered. - """ - if variant not in ("frontoffice", "backoffice"): - variant = "frontoffice" - - _setup_logging() - - app = Flask( - __name__, - template_folder="templates", - static_folder="static", - ) - app.config.from_object(Config) - app.config["APP_VARIANT"] = variant - - # Make sure data dir exists - os.makedirs(os.path.dirname(app.config["DATABASE_PATH"]), exist_ok=True) - - db.init_app(app) - csrf.init_app(app) - login_manager.init_app(app) - login_manager.login_view = "auth.login" - login_manager.login_message = "Please sign in to continue." - login_manager.login_message_category = "info" - - @login_manager.user_loader - def load_user(user_id): - try: - return db.session.get(User, int(user_id)) - except (TypeError, ValueError): - return None - - # ---- Blueprints ---- - # Unified app: the frontoffice lives at the root and the backoffice is - # mounted under /backoffice, so both are served on the SAME port. Access to - # backoffice views is still restricted to admins by @admin_required. - from .routes_auth import auth_bp - from .routes_main import main_bp - from .routes_backoffice import backoffice_bp - - app.register_blueprint(auth_bp) - app.register_blueprint(main_bp) - app.register_blueprint(backoffice_bp, url_prefix="/backoffice") - - # ---- Error handlers ---- - @app.errorhandler(403) - def forbidden(_): - return render_template("error.html", code=403, message="You don't have permission to view this page."), 403 - - @app.errorhandler(404) - def not_found(_): - return render_template("error.html", code=404, message="Page not found."), 404 - - @app.errorhandler(500) - def server_error(_): - return render_template("error.html", code=500, message="Internal server error."), 500 - - # ---- Security headers ---- - @app.after_request - def add_security_headers(response): - response.headers.setdefault("X-Content-Type-Options", "nosniff") - response.headers.setdefault("X-Frame-Options", "DENY") - response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") - response.headers.setdefault("Permissions-Policy", "geolocation=(), microphone=(), camera=()") - # Reasonable CSP for our self-hosted assets + the QR data URI - response.headers.setdefault( - "Content-Security-Policy", - "default-src 'self'; img-src 'self' data:; " - "style-src 'self' 'unsafe-inline'; script-src 'self'; " - "font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" - ) - return response - - # ---- Template context: brand + which area we're in ---- - @app.context_processor - def inject_globals(): - # The backoffice is now mounted under /backoffice on the same origin, - # so cross-links are simple same-site URLs and "is_backoffice" is - # determined by which blueprint served the current request. - is_bo = (request.blueprint == "backoffice") if request else False - try: - backoffice_url = url_for("backoffice.dashboard") - frontoffice_url = url_for("main.index") - except Exception: - backoffice_url = "/backoffice/" - frontoffice_url = "/" - return { - "app_variant": "backoffice" if is_bo else "frontoffice", - "brand_name": "AlertHub", - "is_backoffice": is_bo, - "app_version": APP_VERSION, - "footer_text": "© 2026 Martinhal IT - Joao Vaz - Version " + APP_VERSION, - "backoffice_url": backoffice_url, - "frontoffice_url": frontoffice_url, - } - - # ---- Initialise DB (additive migrations) + seed default settings. - # Serialised across the container's worker processes via a file lock. ---- - _initialise_database(app) - - # ---- First-run guard: if there are no users yet, force the operator - # through the "create administrator" setup screen before anything else. ---- - app._alerthub_has_users = False - - @app.before_request - def _require_initial_setup(): - if app._alerthub_has_users: - return None - # Allow static assets and the setup endpoint itself through. - if request.endpoint in ("static", "auth.setup"): - return None - try: - if User.query.count() > 0: - app._alerthub_has_users = True - return None - except Exception: - return None - return redirect(url_for("auth.setup")) - - # ---- Scheduler. init_scheduler() is idempotent (guarded by a module-level - # singleton), and the container runs a single Gunicorn worker, so exactly - # one reminder scheduler runs regardless of how many times this is called. ---- - init_scheduler(app) - - return app diff --git a/Alert Hub System/app/auth_utils.py b/Alert Hub System/app/auth_utils.py deleted file mode 100644 index ecdf567..0000000 --- a/Alert Hub System/app/auth_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Auth helpers and decorators.""" -from functools import wraps -from flask import abort, redirect, url_for, session, flash, request -from flask_login import current_user - - -def admin_required(view): - @wraps(view) - def wrapped(*args, **kwargs): - if not current_user.is_authenticated: - return redirect(url_for("auth.login", next=request.full_path)) - if not current_user.is_admin: - abort(403) - if session.get("mfa_pending"): - return redirect(url_for("auth.mfa_challenge")) - return view(*args, **kwargs) - return wrapped - - -def login_required_full(view): - """Like flask_login.login_required, but also blocks users with pending MFA.""" - @wraps(view) - def wrapped(*args, **kwargs): - if not current_user.is_authenticated: - return redirect(url_for("auth.login", next=request.full_path)) - if session.get("mfa_pending"): - return redirect(url_for("auth.mfa_challenge")) - return view(*args, **kwargs) - return wrapped - - -def get_remote_ip() -> str: - """Best-effort IP detection for audit logs.""" - fwd = request.headers.get("X-Forwarded-For", "") - if fwd: - return fwd.split(",")[0].strip() - return request.remote_addr or "" diff --git a/Alert Hub System/app/config.py b/Alert Hub System/app/config.py deleted file mode 100644 index ffa9a56..0000000 --- a/Alert Hub System/app/config.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Configuration loaded from environment variables.""" -import os -from datetime import timedelta - - -def _bool(val: str, default: bool = False) -> bool: - if val is None: - return default - return val.strip().lower() in ("1", "true", "yes", "on") - - -class Config: - SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-replace-me") - FLASK_ENV = os.environ.get("FLASK_ENV", "production") - - DATABASE_PATH = os.environ.get("DATABASE_PATH", "/data/alerthub.db") - SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_PATH}" - SQLALCHEMY_TRACK_MODIFICATIONS = False - SQLALCHEMY_ENGINE_OPTIONS = { - "pool_pre_ping": True, - # Wait (instead of erroring) when another process holds the SQLite - # write lock — both gunicorn apps share one database file. - "connect_args": {"timeout": 30}, - } - - # Bootstrap admin (only used on first DB init) - INITIAL_ADMIN_USERNAME = os.environ.get("INITIAL_ADMIN_USERNAME", "admin") - INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL", "admin@example.com") - INITIAL_ADMIN_PASSWORD = os.environ.get("INITIAL_ADMIN_PASSWORD", "ChangeMeOnFirstLogin!") - - # Session security - SESSION_COOKIE_SECURE = _bool(os.environ.get("SESSION_COOKIE_SECURE"), False) - SESSION_COOKIE_HTTPONLY = True - SESSION_COOKIE_SAMESITE = "Lax" - PERMANENT_SESSION_LIFETIME = timedelta( - hours=int(os.environ.get("SESSION_LIFETIME_HOURS", "8")) - ) - - PASSWORD_MIN_LENGTH = int(os.environ.get("PASSWORD_MIN_LENGTH", "10")) - - # MFA - MFA_ISSUER_NAME = os.environ.get("MFA_ISSUER_NAME", "AlertHub") - - # Scheduler - SCHEDULER_INTERVAL_MINUTES = int(os.environ.get("SCHEDULER_INTERVAL_MINUTES", "60")) - - # Default reminder lead times in days - DEFAULT_REMINDER_DAYS = os.environ.get("DEFAULT_REMINDER_DAYS", "30,14,7,1") - - # SMTP defaults (seed only — admins edit live values via backoffice) - # Purelymail outgoing: smtp.purelymail.com : 465 (SSL/TLS) - SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.purelymail.com") - SMTP_PORT = int(os.environ.get("SMTP_PORT", "465")) - SMTP_USERNAME = os.environ.get("SMTP_USERNAME", "") - SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") - SMTP_ENCRYPTION = os.environ.get("SMTP_ENCRYPTION", "ssl") - SMTP_FROM_ADDRESS = os.environ.get("SMTP_FROM_ADDRESS", "") - SMTP_FROM_NAME = os.environ.get("SMTP_FROM_NAME", "AlertHub") - - # IMAP defaults (seed only) — used to save a copy of sent mail to the - # mailbox's Sent folder. Purelymail incoming: imap.purelymail.com : 993 (SSL/TLS) - IMAP_HOST = os.environ.get("IMAP_HOST", "imap.purelymail.com") - IMAP_PORT = int(os.environ.get("IMAP_PORT", "993")) - IMAP_USERNAME = os.environ.get("IMAP_USERNAME", "") - IMAP_PASSWORD = os.environ.get("IMAP_PASSWORD", "") - IMAP_SENT_FOLDER = os.environ.get("IMAP_SENT_FOLDER", "Sent") - - # Ports (used by run scripts only) - FRONTOFFICE_PORT = int(os.environ.get("FRONTOFFICE_PORT", "8080")) - BACKOFFICE_PORT = int(os.environ.get("BACKOFFICE_PORT", "9001")) - - # Optional explicit public URLs for the cross-console links in the top bar. - # If empty, the app derives them from the request host + the ports above - # (e.g. http://:9001/). Set these when behind a reverse proxy - # or when the consoles are reached at custom hostnames/paths. - BACKOFFICE_PUBLIC_URL = os.environ.get("BACKOFFICE_PUBLIC_URL", "") - FRONTOFFICE_PUBLIC_URL = os.environ.get("FRONTOFFICE_PUBLIC_URL", "") - - # Resource limits - MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # 2MB upload cap - WTF_CSRF_TIME_LIMIT = 3600 diff --git a/Alert Hub System/app/forms.py b/Alert Hub System/app/forms.py deleted file mode 100644 index 0947d0e..0000000 --- a/Alert Hub System/app/forms.py +++ /dev/null @@ -1,139 +0,0 @@ -"""WTForms — all forms get CSRF protection automatically via Flask-WTF.""" -from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileAllowed, FileRequired -from wtforms import StringField, PasswordField, BooleanField, IntegerField, SelectField, DateField, TextAreaField, SubmitField -from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, NumberRange, Regexp - - -class LoginForm(FlaskForm): - username = StringField("Username", validators=[DataRequired(), Length(min=2, max=64)]) - password = PasswordField("Password", validators=[DataRequired(), Length(min=1, max=200)]) - submit = SubmitField("Sign in") - - -class SetupAdminForm(FlaskForm): - """First-run form: create the very first administrator account.""" - username = StringField("Admin username", validators=[ - DataRequired(), - Length(min=2, max=64), - Regexp(r"^[A-Za-z0-9_.\-]+$", message="Letters, digits, underscore, dot, dash only."), - ]) - email = StringField("Admin email", validators=[DataRequired(), Email(), Length(max=255)]) - password = PasswordField("Password", validators=[ - DataRequired(), - Length(min=10, max=200, message="Min 10 characters"), - ]) - confirm = PasswordField("Confirm password", validators=[ - DataRequired(), - EqualTo("password", message="Passwords must match."), - ]) - submit = SubmitField("Create administrator") - - -class AvatarForm(FlaskForm): - avatar = FileField("Avatar image", validators=[ - FileRequired(message="Choose an image file."), - FileAllowed(["png", "jpg", "jpeg", "gif", "webp"], "Images only (png, jpg, gif, webp)."), - ]) - submit = SubmitField("Upload avatar") - - -class MfaForm(FlaskForm): - code = StringField("Authentication code", validators=[ - DataRequired(), - Length(min=6, max=6, message="Must be 6 digits"), - Regexp(r"^\d{6}$", message="Digits only"), - ]) - submit = SubmitField("Verify") - - -class MfaSetupForm(FlaskForm): - code = StringField("Verification code", validators=[ - DataRequired(), - Length(min=6, max=6), - Regexp(r"^\d{6}$"), - ]) - submit = SubmitField("Enable MFA") - - -class ChangePasswordForm(FlaskForm): - current_password = PasswordField("Current password", validators=[DataRequired()]) - new_password = PasswordField("New password", validators=[ - DataRequired(), - Length(min=10, max=200, message="Min 10 characters"), - ]) - confirm = PasswordField("Confirm new password", validators=[ - DataRequired(), - EqualTo("new_password", message="Passwords must match."), - ]) - submit = SubmitField("Change password") - - -class AlertForm(FlaskForm): - title = StringField("Title", validators=[DataRequired(), Length(max=200)]) - category = StringField("Category", validators=[Optional(), Length(max=64)]) - description = TextAreaField("Description", validators=[Optional(), Length(max=5000)]) - expiration_date = DateField("Expiration date", validators=[DataRequired()], format="%Y-%m-%d") - reminder_days = StringField( - "Reminder days (comma-separated, days before expiration)", - validators=[DataRequired(), Regexp(r"^[\d,\s]+$", message="Numbers separated by commas only")], - default="30,14,7,1", - ) - is_active = BooleanField("Active", default=True) - submit = SubmitField("Save") - - -class UserForm(FlaskForm): - username = StringField("Username", validators=[ - DataRequired(), - Length(min=2, max=64), - Regexp(r"^[A-Za-z0-9_.\-]+$", message="Letters, digits, underscore, dot, dash only."), - ]) - email = StringField("Email", validators=[DataRequired(), Email(), Length(max=255)]) - role = SelectField("Role", choices=[("user", "User (view only)"), ("admin", "Administrator")]) - is_active = BooleanField("Active", default=True) - password = PasswordField("Password (leave blank to keep current)", validators=[Optional(), Length(min=10, max=200)]) - submit = SubmitField("Save") - - -class MailConfigForm(FlaskForm): - smtp_host = StringField("SMTP Host", validators=[DataRequired(), Length(max=255)]) - smtp_port = IntegerField("Port", validators=[DataRequired(), NumberRange(min=1, max=65535)]) - smtp_username = StringField("Username", validators=[Optional(), Length(max=255)]) - smtp_password = PasswordField("Password (leave blank to keep current)", validators=[Optional(), Length(max=255)]) - smtp_encryption = SelectField("Encryption", choices=[ - ("none", "None (plain — not recommended)"), - ("starttls", "STARTTLS (port 587)"), - ("ssl", "SSL/TLS (port 465)"), - ]) - smtp_from_address = StringField("From address", validators=[DataRequired(), Email(), Length(max=255)]) - smtp_from_name = StringField("From name", validators=[Optional(), Length(max=255)]) - - # ---- IMAP: used to save a copy of every sent message to the Sent folder ---- - imap_enabled = BooleanField("Save a copy of sent mail to the mailbox (IMAP)", default=True) - imap_host = StringField("IMAP Host", validators=[Optional(), Length(max=255)]) - imap_port = IntegerField("IMAP Port", validators=[Optional(), NumberRange(min=1, max=65535)]) - imap_username = StringField("IMAP Username (blank = use SMTP username)", validators=[Optional(), Length(max=255)]) - imap_password = PasswordField("IMAP Password (blank = keep current / use SMTP password)", validators=[Optional(), Length(max=255)]) - imap_sent_folder = StringField("Sent folder name", validators=[Optional(), Length(max=128)]) - - submit = SubmitField("Save mail configuration") - - -class TestMailForm(FlaskForm): - test_recipient = StringField("Send test email to", validators=[DataRequired(), Email()]) - submit = SubmitField("Send test email") - - -class LogEmailForm(FlaskForm): - """Email an exported log (CSV attachment) to a destination address.""" - log_recipient = StringField("Email log to", validators=[DataRequired(), Email()]) - submit = SubmitField("Email log") - - -class GlobalSettingsForm(FlaskForm): - default_reminder_days = StringField( - "Default reminder days (comma-separated)", - validators=[DataRequired(), Regexp(r"^[\d,\s]+$")], - ) - submit = SubmitField("Save settings") diff --git a/Alert Hub System/app/mailer.py b/Alert Hub System/app/mailer.py deleted file mode 100644 index d1edfa5..0000000 --- a/Alert Hub System/app/mailer.py +++ /dev/null @@ -1,161 +0,0 @@ -"""SMTP mailer that pulls live config from the AppSetting store. - -Every send is recorded in the MailLog (visible in the backoffice "Mail Log"), -and — when IMAP is configured — a copy of the outgoing message is appended to -the external mailbox's Sent folder so it shows up in normal mail clients. -""" -import smtplib -import imaplib -import ssl -import time -import logging -from email.message import EmailMessage -from email.utils import formataddr, formatdate -from flask_login import current_user -from .models import AppSetting, MailLog - -logger = logging.getLogger(__name__) - - -def get_mail_config() -> dict: - """Read SMTP + IMAP settings from the database.""" - return { - # ---- SMTP (outgoing) ---- - "host": AppSetting.get("smtp_host", "") or "", - "port": int(AppSetting.get("smtp_port", "465") or "465"), - "username": AppSetting.get("smtp_username", "") or "", - "password": AppSetting.get("smtp_password", "") or "", - "encryption": (AppSetting.get("smtp_encryption", "ssl") or "ssl").lower(), - "from_address": AppSetting.get("smtp_from_address", "") or "", - "from_name": AppSetting.get("smtp_from_name", "AlertHub") or "AlertHub", - # ---- IMAP (for saving a copy in the Sent folder) ---- - "imap_enabled": (AppSetting.get("imap_enabled", "true") or "true").lower() in ("1", "true", "yes", "on"), - "imap_host": AppSetting.get("imap_host", "") or "", - "imap_port": int(AppSetting.get("imap_port", "993") or "993"), - "imap_username": AppSetting.get("imap_username", "") or "", - "imap_password": AppSetting.get("imap_password", "") or "", - "imap_sent_folder": AppSetting.get("imap_sent_folder", "Sent") or "Sent", - } - - -def _build_message(cfg, to_addresses, subject, body_text, body_html=None, attachments=None) -> EmailMessage: - msg = EmailMessage() - msg["Subject"] = subject - msg["From"] = formataddr((cfg["from_name"], cfg["from_address"])) - msg["To"] = ", ".join(to_addresses) - msg["Date"] = formatdate(localtime=True) - msg.set_content(body_text) - if body_html: - msg.add_alternative(body_html, subtype="html") - for att in (attachments or []): - # att = (filename, mime_maintype, mime_subtype, bytes) - fname, maintype, subtype, data = att - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=fname) - return msg - - -def _append_to_sent(cfg, msg) -> tuple[bool, str]: - """Append a copy of the message to the mailbox's Sent folder over IMAP.""" - if not cfg.get("imap_enabled"): - return False, "disabled" - imap_host = cfg["imap_host"] or "" - imap_user = cfg["imap_username"] or cfg["username"] - imap_pass = cfg["imap_password"] or cfg["password"] - if not imap_host or not imap_user or not imap_pass: - return False, "IMAP not fully configured" - folder = cfg.get("imap_sent_folder") or "Sent" - try: - context = ssl.create_default_context() - with imaplib.IMAP4_SSL(imap_host, cfg.get("imap_port", 993), ssl_context=context, timeout=20) as imap: - imap.login(imap_user, imap_pass) - raw = msg.as_bytes() - # \Seen so it doesn't show as unread in the Sent folder - typ, _ = imap.append(folder, "(\\Seen)", imaplib.Time2Internaldate(time.time()), raw) - if typ != "OK": - return False, f"IMAP append returned {typ}" - return True, "saved" - except (imaplib.IMAP4.error, ssl.SSLError, OSError) as e: - logger.exception("IMAP append to Sent failed") - return False, f"IMAP error: {e}" - - -def _smtp_send(cfg, msg) -> tuple[bool, str]: - encryption = cfg["encryption"] - try: - if encryption == "ssl": - context = ssl.create_default_context() - with smtplib.SMTP_SSL(cfg["host"], cfg["port"], context=context, timeout=20) as server: - if cfg["username"]: - server.login(cfg["username"], cfg["password"]) - server.send_message(msg) - else: - with smtplib.SMTP(cfg["host"], cfg["port"], timeout=20) as server: - server.ehlo() - if encryption == "starttls": - context = ssl.create_default_context() - server.starttls(context=context) - server.ehlo() - if cfg["username"]: - server.login(cfg["username"], cfg["password"]) - server.send_message(msg) - return True, "ok" - except smtplib.SMTPAuthenticationError as e: - logger.exception("SMTP auth failed") - err = e.smtp_error.decode("utf-8", errors="ignore") if isinstance(e.smtp_error, bytes) else e.smtp_error - return False, f"Authentication failed: {e.smtp_code} {err}" - except (smtplib.SMTPException, ssl.SSLError, OSError) as e: - logger.exception("SMTP error") - return False, f"SMTP error: {e}" - - -def send_mail(to_addresses, subject: str, body_text: str, body_html: str | None = None, - attachments=None, kind: str = "general", triggered_by: str | None = None) -> tuple[bool, str]: - """Send an email. Returns (success, message). Never raises. - - Side effects: - - Appends a copy to the external mailbox Sent folder (IMAP) on success. - - Records the send (and Sent-copy result) in the MailLog. - """ - cfg = get_mail_config() - - # Resolve who triggered this (username) for the log. - if triggered_by is None: - try: - triggered_by = current_user.username if current_user.is_authenticated else "system" - except Exception: - triggered_by = "system" - - if not cfg["host"] or not cfg["from_address"]: - info = "SMTP not configured (host or from-address missing)." - MailLog.record(kind, to_addresses, subject, "failed", info, "n/a", triggered_by) - return False, info - - if isinstance(to_addresses, str): - to_addresses = [to_addresses] - to_addresses = [a for a in to_addresses if a] - if not to_addresses: - info = "No recipients." - MailLog.record(kind, "", subject, "failed", info, "n/a", triggered_by) - return False, info - - msg = _build_message(cfg, to_addresses, subject, body_text, body_html, attachments) - - ok, info = _smtp_send(cfg, msg) - - sent_copy = "n/a" - if ok: - copied, copy_info = _append_to_sent(cfg, msg) - if copy_info == "disabled": - sent_copy = "disabled" - elif copied: - sent_copy = "saved" - else: - sent_copy = "failed" - info = f"{info}; Sent-copy: {copy_info}" - - MailLog.record(kind, to_addresses, subject, - "sent" if ok else "failed", info, sent_copy, triggered_by) - - if ok: - return True, f"Sent to {len(to_addresses)} recipient(s)." - return False, info diff --git a/Alert Hub System/app/models.py b/Alert Hub System/app/models.py deleted file mode 100644 index b125171..0000000 --- a/Alert Hub System/app/models.py +++ /dev/null @@ -1,192 +0,0 @@ -"""SQLAlchemy models.""" -from datetime import datetime, timezone -from flask_sqlalchemy import SQLAlchemy -from flask_login import UserMixin -import bcrypt -import json - -db = SQLAlchemy() - - -def utcnow(): - return datetime.now(timezone.utc) - - -class User(UserMixin, db.Model): - __tablename__ = "users" - - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(64), unique=True, nullable=False, index=True) - email = db.Column(db.String(255), unique=True, nullable=False) - password_hash = db.Column(db.String(255), nullable=False) - role = db.Column(db.String(16), nullable=False, default="user") # 'user' | 'admin' - is_active = db.Column(db.Boolean, default=True, nullable=False) - - mfa_secret = db.Column(db.String(64), nullable=True) - mfa_enabled = db.Column(db.Boolean, default=False, nullable=False) - - # Optional user avatar (stored as normalised PNG bytes in the DB so it - # survives an "unzip over the top" upgrade without any external files). - avatar_data = db.Column(db.LargeBinary, nullable=True) - avatar_mime = db.Column(db.String(64), nullable=True) - - created_at = db.Column(db.DateTime, default=utcnow, nullable=False) - last_login_at = db.Column(db.DateTime, nullable=True) - failed_login_count = db.Column(db.Integer, default=0, nullable=False) - locked_until = db.Column(db.DateTime, nullable=True) - - # ---- password helpers ---- - def set_password(self, password: str) -> None: - self.password_hash = bcrypt.hashpw( - password.encode("utf-8"), bcrypt.gensalt(rounds=12) - ).decode("utf-8") - - def check_password(self, password: str) -> bool: - try: - return bcrypt.checkpw( - password.encode("utf-8"), self.password_hash.encode("utf-8") - ) - except (ValueError, TypeError): - return False - - @property - def is_admin(self) -> bool: - return self.role == "admin" - - @property - def is_locked(self) -> bool: - return self.locked_until is not None and self.locked_until > utcnow() - - @property - def has_avatar(self) -> bool: - return self.avatar_data is not None and len(self.avatar_data) > 0 - - @property - def initials(self) -> str: - return (self.username or "?")[:2].upper() - - -class Alert(db.Model): - __tablename__ = "alerts" - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(200), nullable=False) - description = db.Column(db.Text, nullable=True) - category = db.Column(db.String(64), nullable=True) # e.g. 'License', 'Renewal' - expiration_date = db.Column(db.Date, nullable=False) - # JSON-encoded list of integer days, e.g. "[30, 14, 7, 1]" - reminder_days_json = db.Column(db.String(255), nullable=False, default="[30,14,7,1]") - # JSON-encoded list of days for which the reminder has already been sent (avoid dupes) - reminders_sent_json = db.Column(db.String(255), nullable=False, default="[]") - is_active = db.Column(db.Boolean, default=True, nullable=False) - created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) - created_at = db.Column(db.DateTime, default=utcnow, nullable=False) - updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow, nullable=False) - - creator = db.relationship("User", foreign_keys=[created_by]) - - @property - def reminder_days(self) -> list: - try: - return sorted(set(int(x) for x in json.loads(self.reminder_days_json)), reverse=True) - except (ValueError, TypeError): - return [] - - @reminder_days.setter - def reminder_days(self, value) -> None: - self.reminder_days_json = json.dumps(sorted(set(int(x) for x in value), reverse=True)) - - @property - def reminders_sent(self) -> list: - try: - return list(json.loads(self.reminders_sent_json)) - except (ValueError, TypeError): - return [] - - @reminders_sent.setter - def reminders_sent(self, value) -> None: - self.reminders_sent_json = json.dumps(list(value)) - - -class AppSetting(db.Model): - """Generic key/value settings store (mail config + misc).""" - __tablename__ = "app_settings" - - key = db.Column(db.String(64), primary_key=True) - value = db.Column(db.Text, nullable=True) - updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow, nullable=False) - - @classmethod - def get(cls, key: str, default=None): - row = db.session.get(cls, key) - return row.value if row is not None else default - - @classmethod - def set(cls, key: str, value: str) -> None: - row = db.session.get(cls, key) - if row is None: - row = cls(key=key, value=value) - db.session.add(row) - else: - row.value = value - db.session.commit() - - @classmethod - def get_all(cls) -> dict: - return {row.key: row.value for row in cls.query.all()} - - -class AuditLog(db.Model): - __tablename__ = "audit_log" - - id = db.Column(db.Integer, primary_key=True) - timestamp = db.Column(db.DateTime, default=utcnow, nullable=False, index=True) - user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) - username = db.Column(db.String(64), nullable=True) # denormalised for survival after delete - action = db.Column(db.String(64), nullable=False) - details = db.Column(db.Text, nullable=True) - ip_address = db.Column(db.String(45), nullable=True) - - @staticmethod - def record(action: str, user=None, details: str = "", ip: str = ""): - entry = AuditLog( - action=action, - user_id=getattr(user, "id", None), - username=getattr(user, "username", None), - details=details[:1000] if details else None, - ip_address=ip, - ) - db.session.add(entry) - db.session.commit() - - -class MailLog(db.Model): - """Record of every outbound email dispatched by the platform.""" - __tablename__ = "mail_log" - - id = db.Column(db.Integer, primary_key=True) - timestamp = db.Column(db.DateTime, default=utcnow, nullable=False, index=True) - kind = db.Column(db.String(32), nullable=False, default="general") # reminder | test | log-export | general - recipients = db.Column(db.Text, nullable=True) - subject = db.Column(db.String(500), nullable=True) - status = db.Column(db.String(16), nullable=False, default="unknown") # sent | failed - info = db.Column(db.Text, nullable=True) - sent_copy = db.Column(db.String(24), nullable=False, default="n/a") # saved | failed | disabled | n/a - triggered_by = db.Column(db.String(64), nullable=True) # username or "system" - - @staticmethod - def record(kind, recipients, subject, status, info="", sent_copy="n/a", triggered_by="system"): - if isinstance(recipients, (list, tuple)): - recipients = ", ".join(str(r) for r in recipients) - entry = MailLog( - kind=(kind or "general")[:32], - recipients=(recipients or "")[:2000], - subject=(subject or "")[:500], - status=(status or "unknown")[:16], - info=(info or "")[:2000], - sent_copy=(sent_copy or "n/a")[:24], - triggered_by=(triggered_by or "system")[:64], - ) - db.session.add(entry) - db.session.commit() - return entry diff --git a/Alert Hub System/app/routes_auth.py b/Alert Hub System/app/routes_auth.py deleted file mode 100644 index 5a094c0..0000000 --- a/Alert Hub System/app/routes_auth.py +++ /dev/null @@ -1,308 +0,0 @@ -"""Authentication routes: first-run setup, login, MFA, logout, password, avatar.""" -import io -import base64 -from datetime import timedelta -import pyotp -import qrcode -from PIL import Image, ImageOps -from flask import ( - Blueprint, render_template, redirect, url_for, flash, request, session, - current_app, abort, Response, -) -from flask_login import login_user, logout_user, login_required, current_user - -from .models import db, User, AuditLog, utcnow -from .forms import LoginForm, MfaForm, MfaSetupForm, ChangePasswordForm, SetupAdminForm, AvatarForm -from .auth_utils import get_remote_ip, login_required_full - -auth_bp = Blueprint("auth", __name__) - - -AVATAR_SIZE = 256 - - -def _normalise_avatar(file_storage) -> bytes: - """Open an uploaded image, square-crop + resize, return PNG bytes. - - Raises ValueError if the file is not a usable image. - """ - try: - img = Image.open(file_storage.stream) - img = ImageOps.exif_transpose(img) - img = img.convert("RGBA") if img.mode in ("RGBA", "LA", "P") else img.convert("RGB") - # Center-crop to a square, then resize. - img = ImageOps.fit(img, (AVATAR_SIZE, AVATAR_SIZE), method=Image.LANCZOS) - if img.mode == "RGBA": - background = Image.new("RGBA", img.size, (255, 255, 255, 0)) - img = Image.alpha_composite(background, img) - buf = io.BytesIO() - img.save(buf, format="PNG", optimize=True) - return buf.getvalue() - except Exception as exc: # noqa: BLE001 - surface a clean error to the user - raise ValueError(f"Could not process image: {exc}") - - -# --------------------------------------------------------------------------- # -# First-run setup — create the initial administrator -# --------------------------------------------------------------------------- # -@auth_bp.route("/setup", methods=["GET", "POST"]) -def setup(): - # Only available while there are zero users. Once an admin exists, hide it. - if User.query.count() > 0: - return redirect(url_for("auth.login")) - - form = SetupAdminForm() - if form.validate_on_submit(): - admin = User( - username=form.username.data.strip(), - email=form.email.data.strip().lower(), - role="admin", - is_active=True, - ) - admin.set_password(form.password.data) - db.session.add(admin) - db.session.commit() - current_app._alerthub_has_users = True - AuditLog.record("setup.admin_created", user=admin, details=f"username={admin.username}", ip=get_remote_ip()) - flash("Administrator account created. Please sign in.", "success") - return redirect(url_for("auth.login")) - - return render_template("setup.html", form=form) - - -# --------------------------------------------------------------------------- # -# Login -# --------------------------------------------------------------------------- # -@auth_bp.route("/login", methods=["GET", "POST"]) -def login(): - if current_user.is_authenticated and not session.get("mfa_pending"): - return redirect(url_for("main.index")) - - form = LoginForm() - if form.validate_on_submit(): - user = User.query.filter_by(username=form.username.data.strip()).first() - ip = get_remote_ip() - - if user is None or not user.check_password(form.password.data): - # Increment failed login if user exists - if user is not None: - user.failed_login_count = (user.failed_login_count or 0) + 1 - if user.failed_login_count >= 5: - user.locked_until = utcnow() + timedelta(minutes=15) - user.failed_login_count = 0 - AuditLog.record("login.lockout", user=user, details=f"5 failed attempts", ip=ip) - db.session.commit() - AuditLog.record("login.fail", details=f"username={form.username.data}", ip=ip) - flash("Invalid credentials.", "error") - return render_template("login.html", form=form), 401 - - if not user.is_active: - flash("Account is disabled.", "error") - AuditLog.record("login.disabled", user=user, ip=ip) - return render_template("login.html", form=form), 403 - - if user.is_locked: - flash("Account is temporarily locked due to failed login attempts. Try again later.", "error") - AuditLog.record("login.locked", user=user, ip=ip) - return render_template("login.html", form=form), 423 - - # Password OK — reset counters - user.failed_login_count = 0 - user.locked_until = None - - if user.mfa_enabled: - # Stage 1 success — but require MFA before granting full session - session["mfa_pending"] = True - session["pending_user_id"] = user.id - db.session.commit() - AuditLog.record("login.password_ok", user=user, ip=ip) - return redirect(url_for("auth.mfa_challenge")) - - # No MFA — full login - login_user(user, remember=False) - session.permanent = True - session.pop("mfa_pending", None) - session.pop("pending_user_id", None) - user.last_login_at = utcnow() - db.session.commit() - AuditLog.record("login.success", user=user, ip=ip) - - next_url = request.args.get("next") or url_for("main.index") - return redirect(next_url) - - return render_template("login.html", form=form) - - -# --------------------------------------------------------------------------- # -# MFA challenge (after password OK) -# --------------------------------------------------------------------------- # -@auth_bp.route("/mfa", methods=["GET", "POST"]) -def mfa_challenge(): - if not session.get("mfa_pending"): - return redirect(url_for("auth.login")) - - user_id = session.get("pending_user_id") - user = db.session.get(User, user_id) if user_id else None - if user is None or not user.mfa_enabled or not user.mfa_secret: - session.pop("mfa_pending", None) - session.pop("pending_user_id", None) - return redirect(url_for("auth.login")) - - form = MfaForm() - if form.validate_on_submit(): - totp = pyotp.TOTP(user.mfa_secret) - if totp.verify(form.code.data, valid_window=1): - login_user(user, remember=False) - session.permanent = True - session.pop("mfa_pending", None) - session.pop("pending_user_id", None) - user.last_login_at = utcnow() - db.session.commit() - AuditLog.record("login.mfa_ok", user=user, ip=get_remote_ip()) - return redirect(url_for("main.index")) - flash("Invalid authentication code.", "error") - AuditLog.record("login.mfa_fail", user=user, ip=get_remote_ip()) - - return render_template("mfa.html", form=form) - - -# --------------------------------------------------------------------------- # -# Logout -# --------------------------------------------------------------------------- # -@auth_bp.route("/logout", methods=["POST"]) -@login_required -def logout(): - AuditLog.record("logout", user=current_user, ip=get_remote_ip()) - logout_user() - session.clear() - flash("Signed out.", "success") - return redirect(url_for("auth.login")) - - -# --------------------------------------------------------------------------- # -# Account: change password / set up MFA -# --------------------------------------------------------------------------- # -@auth_bp.route("/account", methods=["GET"]) -@login_required_full -def account(): - return render_template( - "account.html", - password_form=ChangePasswordForm(), - mfa_setup_form=MfaSetupForm(), - avatar_form=AvatarForm(), - ) - - -# --------------------------------------------------------------------------- # -# Avatar: upload / remove / serve -# --------------------------------------------------------------------------- # -@auth_bp.route("/account/avatar", methods=["POST"]) -@login_required_full -def avatar_upload(): - form = AvatarForm() - if form.validate_on_submit(): - try: - png_bytes = _normalise_avatar(form.avatar.data) - except ValueError as e: - flash(str(e), "error") - return redirect(url_for("auth.account")) - current_user.avatar_data = png_bytes - current_user.avatar_mime = "image/png" - db.session.commit() - AuditLog.record("avatar.update", user=current_user, ip=get_remote_ip()) - flash("Avatar updated.", "success") - else: - for errs in form.errors.values(): - for e in errs: - flash(e, "error") - return redirect(url_for("auth.account")) - - -@auth_bp.route("/account/avatar/remove", methods=["POST"]) -@login_required_full -def avatar_remove(): - current_user.avatar_data = None - current_user.avatar_mime = None - db.session.commit() - AuditLog.record("avatar.remove", user=current_user, ip=get_remote_ip()) - flash("Avatar removed.", "info") - return redirect(url_for("auth.account")) - - -@auth_bp.route("/avatar/", methods=["GET"]) -@login_required_full -def avatar(user_id: int): - user = db.session.get(User, user_id) - if user is None or not user.has_avatar: - abort(404) - resp = Response(user.avatar_data, mimetype=user.avatar_mime or "image/png") - resp.headers["Cache-Control"] = "private, max-age=60" - return resp - - -@auth_bp.route("/account/password", methods=["POST"]) -@login_required_full -def change_password(): - form = ChangePasswordForm() - if form.validate_on_submit(): - if not current_user.check_password(form.current_password.data): - flash("Current password is incorrect.", "error") - else: - current_user.set_password(form.new_password.data) - db.session.commit() - AuditLog.record("password.change", user=current_user, ip=get_remote_ip()) - flash("Password updated.", "success") - return redirect(url_for("auth.account")) - return render_template( - "account.html", - password_form=form, - mfa_setup_form=MfaSetupForm(), - ) - - -@auth_bp.route("/account/mfa/setup", methods=["GET", "POST"]) -@login_required_full -def mfa_setup(): - """Generate a new TOTP secret + QR code; user confirms with a 6-digit code.""" - if current_user.mfa_enabled: - flash("MFA is already enabled.", "info") - return redirect(url_for("auth.account")) - - # Keep candidate secret in session until verified - if "mfa_setup_secret" not in session: - session["mfa_setup_secret"] = pyotp.random_base32() - - secret = session["mfa_setup_secret"] - issuer = current_app.config.get("MFA_ISSUER_NAME", "AlertHub") - uri = pyotp.TOTP(secret).provisioning_uri(name=current_user.email, issuer_name=issuer) - - # Generate QR code as base64 PNG - img = qrcode.make(uri) - buf = io.BytesIO() - img.save(buf, format="PNG") - qr_b64 = base64.b64encode(buf.getvalue()).decode("ascii") - - form = MfaSetupForm() - if form.validate_on_submit(): - if pyotp.TOTP(secret).verify(form.code.data, valid_window=1): - current_user.mfa_secret = secret - current_user.mfa_enabled = True - db.session.commit() - session.pop("mfa_setup_secret", None) - AuditLog.record("mfa.enabled", user=current_user, ip=get_remote_ip()) - flash("MFA enabled successfully. You will be prompted for a code on next sign-in.", "success") - return redirect(url_for("auth.account")) - flash("Verification code did not match. Try again.", "error") - - return render_template("mfa_setup.html", form=form, qr_b64=qr_b64, secret=secret) - - -@auth_bp.route("/account/mfa/disable", methods=["POST"]) -@login_required_full -def mfa_disable(): - current_user.mfa_enabled = False - current_user.mfa_secret = None - db.session.commit() - AuditLog.record("mfa.disabled", user=current_user, ip=get_remote_ip()) - flash("MFA disabled.", "info") - return redirect(url_for("auth.account")) diff --git a/Alert Hub System/app/routes_backoffice.py b/Alert Hub System/app/routes_backoffice.py deleted file mode 100644 index baea9a8..0000000 --- a/Alert Hub System/app/routes_backoffice.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Backoffice routes — admins only. -Sections: - 3. Manage Users & Permissions - - Mail / SMTP configuration - - Global settings (default reminder days, scheduler, run-now) - - Audit log viewer -""" -import csv -import io -from datetime import date, datetime -from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, current_app, Response -from flask_login import current_user - -from .models import db, User, Alert, AppSetting, AuditLog, MailLog -from .forms import UserForm, MailConfigForm, TestMailForm, GlobalSettingsForm, LogEmailForm -from .auth_utils import admin_required, get_remote_ip -from .mailer import send_mail, get_mail_config -from .scheduler import trigger_now - -backoffice_bp = Blueprint("backoffice", __name__) - - -def _csv_response(filename: str, header: list, rows) -> Response: - buf = io.StringIO() - writer = csv.writer(buf) - writer.writerow(header) - for r in rows: - writer.writerow(r) - data = buf.getvalue() - return Response( - data, - mimetype="text/csv", - headers={"Content-Disposition": f'attachment; filename="{filename}"'}, - ) - - -def _audit_rows(): - for e in AuditLog.query.order_by(AuditLog.timestamp.desc()).all(): - yield [ - e.timestamp.strftime("%Y-%m-%d %H:%M:%S") if e.timestamp else "", - e.username or "", - e.action or "", - e.details or "", - e.ip_address or "", - ] - - -def _maillog_rows(): - for m in MailLog.query.order_by(MailLog.timestamp.desc()).all(): - yield [ - m.timestamp.strftime("%Y-%m-%d %H:%M:%S") if m.timestamp else "", - m.kind or "", - m.recipients or "", - m.subject or "", - m.status or "", - m.sent_copy or "", - m.triggered_by or "", - m.info or "", - ] - - -AUDIT_CSV_HEADER = ["timestamp_utc", "user", "action", "details", "ip"] -MAILLOG_CSV_HEADER = ["timestamp_utc", "kind", "recipients", "subject", "status", "sent_copy", "triggered_by", "info"] - - -def _build_csv_string(header, rows) -> str: - buf = io.StringIO() - writer = csv.writer(buf) - writer.writerow(header) - for r in rows: - writer.writerow(r) - return buf.getvalue() - - -# --------------------------------------------------------------------------- # -# Backoffice home — admin dashboard -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/") -@admin_required -def dashboard(): - user_count = User.query.count() - admin_count = User.query.filter_by(role="admin").count() - active_alerts = Alert.query.filter_by(is_active=True).count() - today = date.today() - expiring_soon = Alert.query.filter( - Alert.is_active.is_(True), - Alert.expiration_date >= today, - ).order_by(Alert.expiration_date.asc()).limit(5).all() - recent_audit = AuditLog.query.order_by(AuditLog.timestamp.desc()).limit(10).all() - return render_template( - "backoffice/dashboard.html", - user_count=user_count, - admin_count=admin_count, - active_alerts=active_alerts, - expiring_soon=expiring_soon, - recent_audit=recent_audit, - today=today, - ) - - -# --------------------------------------------------------------------------- # -# User management -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/users") -@admin_required -def users_list(): - users = User.query.order_by(User.username.asc()).all() - return render_template("backoffice/users_list.html", users=users) - - -@backoffice_bp.route("/users/new", methods=["GET", "POST"]) -@admin_required -def user_new(): - form = UserForm() - if form.validate_on_submit(): - if not form.password.data: - flash("Password is required for a new user.", "error") - elif User.query.filter((User.username == form.username.data) | (User.email == form.email.data)).first(): - flash("Username or email already exists.", "error") - elif len(form.password.data) < current_app.config["PASSWORD_MIN_LENGTH"]: - flash(f"Password must be at least {current_app.config['PASSWORD_MIN_LENGTH']} characters.", "error") - else: - user = User( - username=form.username.data.strip(), - email=form.email.data.strip().lower(), - role=form.role.data, - is_active=form.is_active.data, - ) - user.set_password(form.password.data) - db.session.add(user) - db.session.commit() - AuditLog.record("user.create", user=current_user, details=f"new={user.username} role={user.role}", ip=get_remote_ip()) - flash(f"User '{user.username}' created.", "success") - return redirect(url_for("backoffice.users_list")) - return render_template("backoffice/user_form.html", form=form, user=None) - - -@backoffice_bp.route("/users//edit", methods=["GET", "POST"]) -@admin_required -def user_edit(user_id: int): - user = db.session.get(User, user_id) - if user is None: - abort(404) - form = UserForm(obj=user) - if form.validate_on_submit(): - # Username / email uniqueness check (excluding self) - clash = User.query.filter( - User.id != user.id, - (User.username == form.username.data) | (User.email == form.email.data.lower()) - ).first() - if clash: - flash("Another user already has that username or email.", "error") - else: - # Prevent self-demotion of last admin / self-deactivation lock-out - if user.id == current_user.id: - if form.role.data != "admin": - flash("You cannot demote yourself.", "error") - return render_template("backoffice/user_form.html", form=form, user=user) - if not form.is_active.data: - flash("You cannot deactivate your own account.", "error") - return render_template("backoffice/user_form.html", form=form, user=user) - - # Prevent removing the last active admin - if user.role == "admin" and form.role.data != "admin": - other_admins = User.query.filter(User.id != user.id, User.role == "admin", User.is_active.is_(True)).count() - if other_admins == 0: - flash("Cannot demote the last administrator.", "error") - return render_template("backoffice/user_form.html", form=form, user=user) - - user.username = form.username.data.strip() - user.email = form.email.data.strip().lower() - user.role = form.role.data - user.is_active = form.is_active.data - if form.password.data: - if len(form.password.data) < current_app.config["PASSWORD_MIN_LENGTH"]: - flash(f"Password must be at least {current_app.config['PASSWORD_MIN_LENGTH']} characters.", "error") - return render_template("backoffice/user_form.html", form=form, user=user) - user.set_password(form.password.data) - db.session.commit() - AuditLog.record("user.update", user=current_user, details=f"id={user.id} username={user.username}", ip=get_remote_ip()) - flash("User updated.", "success") - return redirect(url_for("backoffice.users_list")) - return render_template("backoffice/user_form.html", form=form, user=user) - - -@backoffice_bp.route("/users//delete", methods=["POST"]) -@admin_required -def user_delete(user_id: int): - user = db.session.get(User, user_id) - if user is None: - abort(404) - if user.id == current_user.id: - flash("You cannot delete your own account.", "error") - return redirect(url_for("backoffice.users_list")) - if user.role == "admin": - other_admins = User.query.filter(User.id != user.id, User.role == "admin", User.is_active.is_(True)).count() - if other_admins == 0: - flash("Cannot delete the last administrator.", "error") - return redirect(url_for("backoffice.users_list")) - username = user.username - db.session.delete(user) - db.session.commit() - AuditLog.record("user.delete", user=current_user, details=f"deleted={username}", ip=get_remote_ip()) - flash(f"User '{username}' deleted.", "success") - return redirect(url_for("backoffice.users_list")) - - -@backoffice_bp.route("/users//reset-mfa", methods=["POST"]) -@admin_required -def user_reset_mfa(user_id: int): - user = db.session.get(User, user_id) - if user is None: - abort(404) - user.mfa_enabled = False - user.mfa_secret = None - db.session.commit() - AuditLog.record("user.mfa_reset", user=current_user, details=f"target={user.username}", ip=get_remote_ip()) - flash(f"MFA reset for '{user.username}'. They can re-enrol on next login.", "info") - return redirect(url_for("backoffice.users_list")) - - -@backoffice_bp.route("/users//unlock", methods=["POST"]) -@admin_required -def user_unlock(user_id: int): - user = db.session.get(User, user_id) - if user is None: - abort(404) - user.locked_until = None - user.failed_login_count = 0 - db.session.commit() - AuditLog.record("user.unlock", user=current_user, details=f"target={user.username}", ip=get_remote_ip()) - flash(f"Unlocked '{user.username}'.", "success") - return redirect(url_for("backoffice.users_list")) - - -# --------------------------------------------------------------------------- # -# Mail configuration -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/mail-config", methods=["GET", "POST"]) -@admin_required -def mail_config(): - form = MailConfigForm() - test_form = TestMailForm() - - if request.method == "GET": - cfg = get_mail_config() - form.smtp_host.data = cfg["host"] - form.smtp_port.data = cfg["port"] - form.smtp_username.data = cfg["username"] - form.smtp_encryption.data = cfg["encryption"] - form.smtp_from_address.data = cfg["from_address"] - form.smtp_from_name.data = cfg["from_name"] - form.imap_enabled.data = cfg["imap_enabled"] - form.imap_host.data = cfg["imap_host"] - form.imap_port.data = cfg["imap_port"] - form.imap_username.data = cfg["imap_username"] - form.imap_sent_folder.data = cfg["imap_sent_folder"] - - if form.submit.data and form.validate_on_submit(): - AppSetting.set("smtp_host", form.smtp_host.data.strip()) - AppSetting.set("smtp_port", str(form.smtp_port.data)) - AppSetting.set("smtp_username", form.smtp_username.data.strip()) - if form.smtp_password.data: - AppSetting.set("smtp_password", form.smtp_password.data) - AppSetting.set("smtp_encryption", form.smtp_encryption.data) - AppSetting.set("smtp_from_address", form.smtp_from_address.data.strip().lower()) - AppSetting.set("smtp_from_name", (form.smtp_from_name.data or "").strip() or "AlertHub") - # ---- IMAP (Sent-folder copy) ---- - AppSetting.set("imap_enabled", "true" if form.imap_enabled.data else "false") - AppSetting.set("imap_host", (form.imap_host.data or "").strip()) - AppSetting.set("imap_port", str(form.imap_port.data or 993)) - AppSetting.set("imap_username", (form.imap_username.data or "").strip()) - if form.imap_password.data: - AppSetting.set("imap_password", form.imap_password.data) - AppSetting.set("imap_sent_folder", (form.imap_sent_folder.data or "").strip() or "Sent") - AuditLog.record("mailconfig.update", user=current_user, ip=get_remote_ip()) - flash("Mail configuration saved.", "success") - return redirect(url_for("backoffice.mail_config")) - - return render_template("backoffice/mail_config.html", form=form, test_form=test_form) - - -@backoffice_bp.route("/mail-config/test", methods=["POST"]) -@admin_required -def mail_test(): - test_form = TestMailForm() - if test_form.validate_on_submit(): - ok, info = send_mail( - test_form.test_recipient.data.strip(), - "[AlertHub] Test email", - "This is a test email from AlertHub. If you received this, SMTP is configured correctly.", - "

This is a test email from AlertHub. If you received this, SMTP is configured correctly.

", - kind="test", - triggered_by=current_user.username, - ) - AuditLog.record("mailconfig.test", user=current_user, details=f"to={test_form.test_recipient.data} ok={ok}", ip=get_remote_ip()) - flash(("Test email sent. " if ok else "Test failed: ") + info, "success" if ok else "error") - else: - flash("Provide a valid recipient email address.", "error") - return redirect(url_for("backoffice.mail_config")) - - -# --------------------------------------------------------------------------- # -# Global settings + scheduler trigger -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/settings", methods=["GET", "POST"]) -@admin_required -def settings(): - form = GlobalSettingsForm() - if request.method == "GET": - form.default_reminder_days.data = AppSetting.get("default_reminder_days", "30,14,7,1") - if form.validate_on_submit(): - cleaned = ",".join( - str(x) for x in sorted({int(p.strip()) for p in form.default_reminder_days.data.split(",") if p.strip().isdigit()}, reverse=True) - ) - if not cleaned: - flash("Please enter at least one valid day value.", "error") - else: - AppSetting.set("default_reminder_days", cleaned) - AuditLog.record("settings.update", user=current_user, details=f"default_reminder_days={cleaned}", ip=get_remote_ip()) - flash("Settings saved.", "success") - return redirect(url_for("backoffice.settings")) - scheduler_interval = current_app.config.get("SCHEDULER_INTERVAL_MINUTES", 60) - return render_template("backoffice/settings.html", form=form, scheduler_interval=scheduler_interval) - - -@backoffice_bp.route("/settings/run-check", methods=["POST"]) -@admin_required -def run_check_now(): - """Manually run the reminder scan once.""" - try: - trigger_now(current_app._get_current_object()) - AuditLog.record("scheduler.manual_run", user=current_user, ip=get_remote_ip()) - flash("Reminder check executed.", "success") - except Exception as e: - current_app.logger.exception("Manual scheduler run failed") - flash(f"Reminder check failed: {e}", "error") - return redirect(url_for("backoffice.settings")) - - -# --------------------------------------------------------------------------- # -# Audit log viewer -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/audit-log") -@admin_required -def audit_log(): - page = max(1, int(request.args.get("page", 1))) - per_page = 50 - pagination = AuditLog.query.order_by(AuditLog.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False) - return render_template("backoffice/audit_log.html", pagination=pagination, log_email_form=LogEmailForm()) - - -@backoffice_bp.route("/audit-log/export") -@admin_required -def audit_log_export(): - stamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S") - AuditLog.record("auditlog.export", user=current_user, ip=get_remote_ip()) - return _csv_response(f"alerthub-audit-log-{stamp}.csv", AUDIT_CSV_HEADER, _audit_rows()) - - -@backoffice_bp.route("/audit-log/email", methods=["POST"]) -@admin_required -def audit_log_email(): - form = LogEmailForm() - if not form.validate_on_submit(): - flash("Provide a valid destination email address.", "error") - return redirect(url_for("backoffice.audit_log")) - recipient = form.log_recipient.data.strip() - stamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S") - csv_str = _build_csv_string(AUDIT_CSV_HEADER, _audit_rows()) - ok, info = send_mail( - recipient, - f"[AlertHub] Audit log export ({stamp} UTC)", - "Attached is the AlertHub audit log export requested from the backoffice.", - "

Attached is the AlertHub audit log export requested from the backoffice.

", - attachments=[(f"alerthub-audit-log-{stamp}.csv", "text", "csv", csv_str.encode("utf-8"))], - kind="log-export", - triggered_by=current_user.username, - ) - AuditLog.record("auditlog.email", user=current_user, details=f"to={recipient} ok={ok}", ip=get_remote_ip()) - flash(("Audit log emailed. " if ok else "Failed to email log: ") + info, "success" if ok else "error") - return redirect(url_for("backoffice.audit_log")) - - -# --------------------------------------------------------------------------- # -# Mail log viewer (every message the platform sends) -# --------------------------------------------------------------------------- # -@backoffice_bp.route("/mail-log") -@admin_required -def mail_log(): - page = max(1, int(request.args.get("page", 1))) - per_page = 50 - pagination = MailLog.query.order_by(MailLog.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False) - return render_template("backoffice/mail_log.html", pagination=pagination, log_email_form=LogEmailForm()) - - -@backoffice_bp.route("/mail-log/export") -@admin_required -def mail_log_export(): - stamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S") - AuditLog.record("maillog.export", user=current_user, ip=get_remote_ip()) - return _csv_response(f"alerthub-mail-log-{stamp}.csv", MAILLOG_CSV_HEADER, _maillog_rows()) - - -@backoffice_bp.route("/mail-log/email", methods=["POST"]) -@admin_required -def mail_log_email(): - form = LogEmailForm() - if not form.validate_on_submit(): - flash("Provide a valid destination email address.", "error") - return redirect(url_for("backoffice.mail_log")) - recipient = form.log_recipient.data.strip() - stamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S") - csv_str = _build_csv_string(MAILLOG_CSV_HEADER, _maillog_rows()) - ok, info = send_mail( - recipient, - f"[AlertHub] Mail log export ({stamp} UTC)", - "Attached is the AlertHub mail log export requested from the backoffice.", - "

Attached is the AlertHub mail log export requested from the backoffice.

", - attachments=[(f"alerthub-mail-log-{stamp}.csv", "text", "csv", csv_str.encode("utf-8"))], - kind="log-export", - triggered_by=current_user.username, - ) - AuditLog.record("maillog.email", user=current_user, details=f"to={recipient} ok={ok}", ip=get_remote_ip()) - flash(("Mail log emailed. " if ok else "Failed to email log: ") + info, "success" if ok else "error") - return redirect(url_for("backoffice.mail_log")) diff --git a/Alert Hub System/app/routes_main.py b/Alert Hub System/app/routes_main.py deleted file mode 100644 index 4b5ebd1..0000000 --- a/Alert Hub System/app/routes_main.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Frontoffice routes — visible to all authenticated users. - -Sections: - 1. View Data (everyone) - 2. Add/Remove/Edit (admins only) -""" -import json -from datetime import date -from flask import Blueprint, render_template, redirect, url_for, flash, request, abort -from flask_login import current_user - -from .models import db, Alert, AppSetting, AuditLog -from .forms import AlertForm -from .auth_utils import admin_required, login_required_full, get_remote_ip - -main_bp = Blueprint("main", __name__) - - -def _parse_days(text: str) -> list[int]: - return sorted({int(x.strip()) for x in text.split(",") if x.strip().isdigit() and int(x.strip()) >= 0}, reverse=True) - - -# --------------------------------------------------------------------------- # -# Section 1 — View Data -# --------------------------------------------------------------------------- # -@main_bp.route("/") -@login_required_full -def index(): - today = date.today() - alerts = Alert.query.filter_by(is_active=True).order_by(Alert.expiration_date.asc()).all() - payload = [] - for a in alerts: - days_left = (a.expiration_date - today).days - payload.append({ - "id": a.id, - "title": a.title, - "category": a.category, - "description": a.description, - "expiration_date": a.expiration_date.isoformat(), - "days_left": days_left, - "reminder_days": a.reminder_days, - }) - return render_template("index.html", alerts=payload, today_iso=today.isoformat()) - - -# --------------------------------------------------------------------------- # -# Section 2 — Add / Remove / Edit -# --------------------------------------------------------------------------- # -@main_bp.route("/alerts") -@login_required_full -def alerts_list(): - alerts = Alert.query.order_by(Alert.expiration_date.asc()).all() - return render_template("alerts_list.html", alerts=alerts) - - -@main_bp.route("/alerts/new", methods=["GET", "POST"]) -@admin_required -def alert_new(): - default_reminder_days = AppSetting.get("default_reminder_days", "30,14,7,1") - form = AlertForm(reminder_days=default_reminder_days) - if form.validate_on_submit(): - days = _parse_days(form.reminder_days.data) - if not days: - flash("Reminder days must contain at least one valid number.", "error") - else: - alert = Alert( - title=form.title.data.strip(), - category=(form.category.data or "").strip() or None, - description=(form.description.data or "").strip() or None, - expiration_date=form.expiration_date.data, - is_active=form.is_active.data, - created_by=current_user.id, - ) - alert.reminder_days = days - db.session.add(alert) - db.session.commit() - AuditLog.record("alert.create", user=current_user, details=f"id={alert.id} title={alert.title}", ip=get_remote_ip()) - flash("Alert created.", "success") - return redirect(url_for("main.alerts_list")) - return render_template("alert_form.html", form=form, alert=None) - - -@main_bp.route("/alerts//edit", methods=["GET", "POST"]) -@admin_required -def alert_edit(alert_id: int): - alert = db.session.get(Alert, alert_id) - if alert is None: - abort(404) - form = AlertForm(obj=alert) - if request.method == "GET": - form.reminder_days.data = ",".join(str(d) for d in alert.reminder_days) - if form.validate_on_submit(): - days = _parse_days(form.reminder_days.data) - if not days: - flash("Reminder days must contain at least one valid number.", "error") - else: - alert.title = form.title.data.strip() - alert.category = (form.category.data or "").strip() or None - alert.description = (form.description.data or "").strip() or None - alert.expiration_date = form.expiration_date.data - alert.is_active = form.is_active.data - # If reminder_days changed OR expiration_date changed, reset sent log - if days != alert.reminder_days: - alert.reminders_sent = [] - alert.reminder_days = days - db.session.commit() - AuditLog.record("alert.update", user=current_user, details=f"id={alert.id}", ip=get_remote_ip()) - flash("Alert updated.", "success") - return redirect(url_for("main.alerts_list")) - return render_template("alert_form.html", form=form, alert=alert) - - -@main_bp.route("/alerts//delete", methods=["POST"]) -@admin_required -def alert_delete(alert_id: int): - alert = db.session.get(Alert, alert_id) - if alert is None: - abort(404) - title = alert.title - db.session.delete(alert) - db.session.commit() - AuditLog.record("alert.delete", user=current_user, details=f"id={alert_id} title={title}", ip=get_remote_ip()) - flash(f"Alert '{title}' deleted.", "success") - return redirect(url_for("main.alerts_list")) - - -@main_bp.route("/alerts//reset-reminders", methods=["POST"]) -@admin_required -def alert_reset_reminders(alert_id: int): - alert = db.session.get(Alert, alert_id) - if alert is None: - abort(404) - alert.reminders_sent = [] - db.session.commit() - AuditLog.record("alert.reset_reminders", user=current_user, details=f"id={alert_id}", ip=get_remote_ip()) - flash("Reminder send-history cleared. Pending reminders will be re-sent on next scheduler run.", "info") - return redirect(url_for("main.alerts_list")) diff --git a/Alert Hub System/app/scheduler.py b/Alert Hub System/app/scheduler.py deleted file mode 100644 index 4668a6b..0000000 --- a/Alert Hub System/app/scheduler.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Background scheduler that scans alerts and dispatches reminder emails.""" -import logging -from datetime import date, timezone -from apscheduler.schedulers.background import BackgroundScheduler -from .models import Alert, User, db -from .mailer import send_mail - -logger = logging.getLogger(__name__) -_scheduler: BackgroundScheduler | None = None - - -def _check_and_send(app): - """Iterate active alerts; for each reminder threshold reached, email all users.""" - with app.app_context(): - today = date.today() - active_alerts = Alert.query.filter_by(is_active=True).all() - recipients = [u.email for u in User.query.filter_by(is_active=True).all() if u.email] - if not recipients: - logger.info("Scheduler tick: no recipients.") - return - - for alert in active_alerts: - days_left = (alert.expiration_date - today).days - already_sent = set(alert.reminders_sent) - newly_sent = [] - for threshold in alert.reminder_days: - if days_left <= threshold and threshold not in already_sent: - subject = f"[AlertHub] Reminder: {alert.title} expires in {days_left} day(s)" - body_text = ( - f"Alert: {alert.title}\n" - f"Category: {alert.category or '—'}\n" - f"Expiration date: {alert.expiration_date.isoformat()}\n" - f"Days remaining: {days_left}\n\n" - f"{alert.description or ''}\n" - ) - body_html = f""" -
-

⏰ AlertHub Reminder

-

{alert.title}

-

Category: {alert.category or '—'}
- Expiration date: {alert.expiration_date.isoformat()}
- Days remaining: {days_left}

-

{alert.description or ''}

-
- """ - ok, info = send_mail(recipients, subject, body_text, body_html, - kind="reminder", triggered_by="system") - logger.info("Alert %s threshold %d: send=%s (%s)", alert.id, threshold, ok, info) - if ok: - newly_sent.append(threshold) - if newly_sent: - alert.reminders_sent = list(already_sent.union(newly_sent)) - db.session.commit() - - -def init_scheduler(app): - """Start the scheduler if not already running. Called once at app startup.""" - global _scheduler - if _scheduler is not None and _scheduler.running: - return _scheduler - interval = max(1, int(app.config.get("SCHEDULER_INTERVAL_MINUTES", 60))) - _scheduler = BackgroundScheduler(timezone="UTC") - _scheduler.add_job( - _check_and_send, - "interval", - minutes=interval, - args=[app], - id="alert_reminder_job", - replace_existing=True, - next_run_time=None, # don't run immediately on boot - ) - _scheduler.start() - logger.info("Scheduler started; reminder check every %d minute(s).", interval) - return _scheduler - - -def trigger_now(app): - """Manual trigger (for the 'Run check now' button).""" - _check_and_send(app) diff --git a/Alert Hub System/app/static/css/app.css b/Alert Hub System/app/static/css/app.css deleted file mode 100644 index 5b061a8..0000000 --- a/Alert Hub System/app/static/css/app.css +++ /dev/null @@ -1,869 +0,0 @@ -/* ====================================================================== - AlertHub — IT operations console aesthetic - Typography: JetBrains Mono (display/mono), Space Grotesk avoided in favour - of system stack with mono accents to keep the terminal vibe. - ====================================================================== */ - -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap'); - -:root { - --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; - --font-sans: 'IBM Plex Sans', -apple-system, system-ui, sans-serif; - - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 14px; - - --tx-h: 1.15; - --tx-b: 1.55; -} - -/* ---------- Dark theme (default) ---------- */ -[data-theme="dark"] { - --bg-base: #0a0e14; - --bg-elev-1: #0f141b; - --bg-elev-2: #141a23; - --bg-elev-3: #1a212c; - --grid-line: rgba(0, 212, 170, 0.04); - --border: #1f2733; - --border-strong: #2a3442; - - --text-primary: #d8e2ec; - --text-secondary: #8b96a5; - --text-dim: #5a6371; - - --accent: #00d4aa; /* signal green */ - --accent-soft: rgba(0, 212, 170, 0.12); - --accent-hot: #00ffcc; - --magenta: #ff3e8b; /* alert magenta */ - --amber: #ffb454; - --danger: #ff5c5c; - --info: #4ad8ff; - - --shadow-1: 0 1px 0 rgba(255,255,255,0.03), 0 8px 24px rgba(0,0,0,0.5); - --glow-accent: 0 0 0 1px rgba(0,212,170,0.3), 0 0 20px rgba(0,212,170,0.15); -} - -/* ---------- Light theme ---------- */ -[data-theme="light"] { - --bg-base: #f4f6f9; - --bg-elev-1: #ffffff; - --bg-elev-2: #f9fbfd; - --bg-elev-3: #f0f3f7; - --grid-line: rgba(0, 130, 100, 0.05); - --border: #e2e8ef; - --border-strong: #c8d2dd; - - --text-primary: #0e1620; - --text-secondary: #4a5566; - --text-dim: #8693a3; - - --accent: #008f74; - --accent-soft: rgba(0, 143, 116, 0.08); - --accent-hot: #00b388; - --magenta: #d92671; - --amber: #d97a13; - --danger: #d93939; - --info: #1f9ec5; - - --shadow-1: 0 1px 2px rgba(15, 30, 45, 0.06), 0 8px 20px rgba(15, 30, 45, 0.05); - --glow-accent: 0 0 0 1px rgba(0, 143, 116, 0.25); -} - -* { box-sizing: border-box; } - -html, body { - margin: 0; - padding: 0; - background: var(--bg-base); - color: var(--text-primary); - font-family: var(--font-sans); - font-size: 15px; - line-height: var(--tx-b); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - min-height: 100vh; -} - -body { - position: relative; - overflow-x: hidden; -} - -/* Decorative grid background */ -.grid-bg { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - background-image: - linear-gradient(var(--grid-line) 1px, transparent 1px), - linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); - background-size: 32px 32px; - mask-image: radial-gradient(ellipse at top, black 30%, transparent 75%); - -webkit-mask-image: radial-gradient(ellipse at top, black 30%, transparent 75%); -} - -a { color: var(--accent); text-decoration: none; } -a:hover { color: var(--accent-hot); text-decoration: underline; text-underline-offset: 3px; } - -h1, h2, h3, h4 { - font-family: var(--font-sans); - font-weight: 700; - letter-spacing: -0.01em; - line-height: var(--tx-h); - margin: 0 0 .6em; -} -h1 { font-size: 1.85rem; } -h2 { font-size: 1.35rem; } -h3 { font-size: 1.1rem; } - -code, .mono { font-family: var(--font-mono); font-size: .92em; } - -/* ---------- Top bar ---------- */ -.topbar { - position: sticky; - top: 0; - z-index: 50; - backdrop-filter: blur(10px) saturate(140%); - -webkit-backdrop-filter: blur(10px) saturate(140%); - background: color-mix(in srgb, var(--bg-elev-1) 82%, transparent); - border-bottom: 1px solid var(--border); -} -.topbar-inner { - max-width: 1320px; - margin: 0 auto; - padding: 12px 24px; - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 24px; -} -.brand { - display: inline-flex; - align-items: center; - gap: 10px; - color: var(--text-primary); - font-family: var(--font-mono); - font-weight: 700; - letter-spacing: -0.01em; -} -.brand:hover { text-decoration: none; } -.brand-mark { - color: var(--accent); - font-size: 1.1em; - letter-spacing: -2px; - filter: drop-shadow(0 0 6px var(--accent)); -} -.brand-name { font-size: 1.05rem; } -.brand-tag { - font-family: var(--font-mono); - font-size: .65rem; - font-weight: 500; - letter-spacing: 0.15em; - padding: 2px 7px; - border-radius: 4px; - background: var(--accent-soft); - color: var(--accent); - margin-left: 6px; -} -body.is-backoffice .brand-tag { - background: color-mix(in srgb, var(--magenta) 14%, transparent); - color: var(--magenta); -} - -.topbar-nav { - display: flex; - gap: 4px; - justify-self: center; -} -.navlink { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 14px; - font-family: var(--font-mono); - font-size: .85rem; - font-weight: 500; - color: var(--text-secondary); - border-radius: var(--radius-sm); - transition: all .15s ease; -} -.navlink:hover { color: var(--text-primary); background: var(--bg-elev-2); text-decoration: none; } -.navlink.active { color: var(--accent); background: var(--accent-soft); } -.navlink.active .dot { background: var(--accent); box-shadow: 0 0 8px var(--accent); } -.dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--text-dim); -} -.dot-admin { background: var(--magenta); box-shadow: 0 0 8px var(--magenta); } -.navlink-cross { border: 1px solid var(--border); } -.navlink-cross:hover { border-color: var(--border-strong); color: var(--magenta); } -.navlink-cross.active { color: var(--magenta); background: color-mix(in srgb, var(--magenta) 12%, transparent); } -.navlink-cross.active .dot-admin { background: var(--magenta); } - -.topbar-right { display: inline-flex; gap: 10px; align-items: center; justify-self: end; } -.icon-btn { - background: transparent; - border: 1px solid var(--border); - color: var(--text-secondary); - width: 36px; height: 36px; - border-radius: var(--radius-sm); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all .15s ease; -} -.icon-btn:hover { color: var(--text-primary); border-color: var(--border-strong); background: var(--bg-elev-2); } -.icon-sun { display: none; } -.icon-moon { display: block; } -[data-theme="light"] .icon-sun { display: block; } -[data-theme="light"] .icon-moon { display: none; } - -.user-chip { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 4px 12px 4px 4px; - border: 1px solid var(--border); - border-radius: 999px; - color: var(--text-primary); - background: var(--bg-elev-2); -} -.user-chip:hover { border-color: var(--border-strong); text-decoration: none; } -.user-avatar { - width: 28px; height: 28px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent), var(--info)); - color: #061218; - font-family: var(--font-mono); - font-size: .72rem; - font-weight: 700; - display: inline-flex; - align-items: center; - justify-content: center; -} -.user-avatar-img { - object-fit: cover; - padding: 0; - border: 1px solid var(--border-strong); - background: var(--bg-elev-3); -} -.user-meta { display: flex; flex-direction: column; line-height: 1.1; } -.user-name { font-size: .85rem; } -.user-role { - font-family: var(--font-mono); - font-size: .6rem; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; -} -.user-role.role-admin { color: var(--magenta); } -.user-role.role-user { color: var(--text-dim); } - -.inline-form { display: inline; } - -/* ---------- Container ---------- */ -.container { - max-width: 1320px; - margin: 0 auto; - padding: 32px 24px 64px; - position: relative; - z-index: 1; -} - -/* ---------- Buttons ---------- */ -.btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 16px; - border-radius: var(--radius-sm); - font-family: var(--font-mono); - font-size: .85rem; - font-weight: 500; - border: 1px solid transparent; - cursor: pointer; - transition: all .15s ease; - white-space: nowrap; - text-decoration: none; -} -.btn:hover { text-decoration: none; } -.btn-primary { - background: var(--accent); - color: #02181a; - border-color: var(--accent); -} -.btn-primary:hover { background: var(--accent-hot); box-shadow: var(--glow-accent); color: #02181a; } -.btn-ghost { - background: transparent; - color: var(--text-primary); - border-color: var(--border); -} -.btn-ghost:hover { border-color: var(--border-strong); background: var(--bg-elev-2); } -.btn-danger { - background: transparent; - color: var(--danger); - border-color: color-mix(in srgb, var(--danger) 35%, transparent); -} -.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: var(--danger); } -.btn-sm { padding: 5px 10px; font-size: .78rem; } -.btn-block { display: flex; width: 100%; justify-content: center; } -.btn-row { display: flex; gap: 8px; flex-wrap: wrap; } - -/* ---------- Cards / Panels ---------- */ -.panel { - background: var(--bg-elev-1); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-1); -} -.panel-header { - padding: 16px 20px; - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; -} -.panel-header h2, .panel-header h3 { margin: 0; } -.panel-body { padding: 20px; } - -.section-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 16px; - margin: 0 0 24px; - padding-bottom: 16px; - border-bottom: 1px dashed var(--border); -} -.section-head .lead { color: var(--text-secondary); margin: 6px 0 0; font-size: .95rem; } -.section-tag { - font-family: var(--font-mono); - font-size: .65rem; - letter-spacing: 0.15em; - color: var(--accent); - text-transform: uppercase; - margin-bottom: 6px; - display: block; -} - -/* ---------- Forms ---------- */ -.form { display: grid; gap: 16px; } -.form label { - display: block; - font-family: var(--font-mono); - font-size: .78rem; - font-weight: 500; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--text-secondary); - margin-bottom: 6px; -} -.form input[type=text], -.form input[type=email], -.form input[type=password], -.form input[type=number], -.form input[type=date], -.form select, -.form textarea { - width: 100%; - padding: 10px 12px; - background: var(--bg-elev-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: var(--font-sans); - font-size: .95rem; - transition: all .15s ease; -} -.form textarea { font-family: var(--font-sans); resize: vertical; min-height: 90px; } -.form input:focus, .form select:focus, .form textarea:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); - background: var(--bg-elev-1); -} -.form .check-row { - display: inline-flex; - align-items: center; - gap: 10px; -} -.form .check-row label { - margin: 0; - text-transform: none; - letter-spacing: 0; - font-family: var(--font-sans); - font-size: .95rem; - font-weight: 500; - color: var(--text-primary); -} -.help { color: var(--text-dim); font-size: .82rem; margin-top: 4px; } -.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } -@media (max-width: 720px) { .form-grid-2 { grid-template-columns: 1fr; } } - -.error { color: var(--danger); font-size: .82rem; margin-top: 4px; } - -/* ---------- Flash messages ---------- */ -.flash-stack { display: grid; gap: 10px; margin-bottom: 24px; } -.flash { - padding: 12px 16px; - border-radius: var(--radius-sm); - border: 1px solid; - display: flex; - align-items: center; - gap: 12px; - background: var(--bg-elev-1); - font-size: .92rem; -} -.flash-icon { - width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; -} -.flash-success { border-color: color-mix(in srgb, var(--accent) 40%, transparent); } -.flash-success .flash-icon { background: var(--accent); box-shadow: 0 0 8px var(--accent); } -.flash-error { border-color: color-mix(in srgb, var(--danger) 40%, transparent); } -.flash-error .flash-icon { background: var(--danger); box-shadow: 0 0 8px var(--danger); } -.flash-info { border-color: color-mix(in srgb, var(--info) 40%, transparent); } -.flash-info .flash-icon { background: var(--info); } - -/* ---------- Auth pages ---------- */ -.auth-shell { - min-height: calc(100vh - 200px); - display: flex; - align-items: center; - justify-content: center; - padding: 40px 16px; -} -.auth-card { - width: 100%; - max-width: 420px; - background: var(--bg-elev-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 32px; - box-shadow: var(--shadow-1); - position: relative; -} -.auth-card::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - padding: 1px; - background: linear-gradient(135deg, var(--accent), transparent 40%, var(--magenta)); - -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - opacity: .5; - pointer-events: none; -} -.auth-prompt { - font-family: var(--font-mono); - font-size: .8rem; - color: var(--accent); - margin-bottom: 6px; - letter-spacing: 0.06em; -} -.auth-prompt::before { content: '> '; opacity: .7; } -.auth-card h1 { margin-bottom: 6px; font-size: 1.5rem; } -.auth-sub { color: var(--text-secondary); font-size: .9rem; margin-bottom: 24px; } - -/* ---------- ALERT cards (the heart of the View page) ---------- */ -.alerts-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); - gap: 18px; -} -.alert-card { - background: var(--bg-elev-1); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 20px; - position: relative; - overflow: hidden; - transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease; -} -.alert-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-1); border-color: var(--border-strong); } -.alert-card::before { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; - height: 2px; - background: var(--accent); - opacity: .8; -} -.alert-card.is-warning::before { background: var(--amber); } -.alert-card.is-critical::before { background: var(--danger); } -.alert-card.is-expired::before { background: var(--text-dim); } - -.alert-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} -.alert-title { margin: 0; font-size: 1.1rem; font-weight: 700; line-height: 1.3; } -.alert-cat { - display: inline-block; - font-family: var(--font-mono); - font-size: .65rem; - letter-spacing: 0.1em; - text-transform: uppercase; - padding: 3px 8px; - border-radius: 3px; - background: var(--bg-elev-3); - color: var(--text-secondary); -} -.alert-desc { - color: var(--text-secondary); - font-size: .9rem; - margin: 0 0 16px; - line-height: 1.5; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} -.alert-message { - font-family: var(--font-mono); - font-size: .85rem; - color: var(--text-secondary); - margin-bottom: 16px; - padding: 10px 12px; - background: var(--bg-elev-2); - border-left: 2px solid var(--accent); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; -} -.alert-card.is-warning .alert-message { border-left-color: var(--amber); } -.alert-card.is-critical .alert-message { border-left-color: var(--danger); } -.alert-card.is-expired .alert-message { border-left-color: var(--text-dim); } - -/* The countdown timer */ -.countdown { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 6px; -} -.countdown-segment { - background: var(--bg-elev-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 10px 4px 8px; - text-align: center; - position: relative; -} -.countdown-value { - font-family: var(--font-mono); - font-size: 1.5rem; - font-weight: 700; - color: var(--accent); - line-height: 1; - display: block; - letter-spacing: -0.02em; - font-variant-numeric: tabular-nums; -} -.alert-card.is-warning .countdown-value { color: var(--amber); } -.alert-card.is-critical .countdown-value { - color: var(--danger); - animation: pulse-num 1.4s ease-in-out infinite; -} -.alert-card.is-expired .countdown-value { color: var(--text-dim); } -.countdown-label { - font-family: var(--font-mono); - font-size: .55rem; - font-weight: 500; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--text-dim); - display: block; - margin-top: 4px; -} -@keyframes pulse-num { - 0%, 100% { opacity: 1; } - 50% { opacity: .55; } -} - -.alert-meta { - margin-top: 14px; - display: flex; - justify-content: space-between; - align-items: center; - font-family: var(--font-mono); - font-size: .72rem; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.08em; -} -.alert-status-dot { - display: inline-block; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--accent); - margin-right: 6px; - box-shadow: 0 0 6px var(--accent); -} -.alert-card.is-warning .alert-status-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); } -.alert-card.is-critical .alert-status-dot { - background: var(--danger); - box-shadow: 0 0 8px var(--danger); - animation: blink 1s ease-in-out infinite; -} -.alert-card.is-expired .alert-status-dot { background: var(--text-dim); box-shadow: none; } -@keyframes blink { 50% { opacity: .25; } } - -/* Empty state */ -.empty { - text-align: center; - padding: 60px 20px; - color: var(--text-secondary); - border: 1px dashed var(--border); - border-radius: var(--radius-md); -} -.empty .empty-icon { font-size: 2rem; opacity: .5; margin-bottom: 12px; } - -/* ---------- Tables ---------- */ -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - background: var(--bg-elev-1); - border: 1px solid var(--border); - border-radius: var(--radius-md); - overflow: hidden; -} -.table th, .table td { - padding: 12px 16px; - text-align: left; - border-bottom: 1px solid var(--border); - vertical-align: middle; -} -.table thead th { - background: var(--bg-elev-2); - font-family: var(--font-mono); - font-size: .72rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--text-secondary); -} -.table tbody tr:hover { background: var(--bg-elev-2); } -.table tbody tr:last-child td { border-bottom: none; } -.table .actions { text-align: right; white-space: nowrap; } - -.role-badge { - display: inline-block; - font-family: var(--font-mono); - font-size: .68rem; - letter-spacing: 0.1em; - text-transform: uppercase; - padding: 3px 8px; - border-radius: 3px; -} -.role-badge.role-admin { background: color-mix(in srgb, var(--magenta) 14%, transparent); color: var(--magenta); } -.role-badge.role-user { background: var(--bg-elev-3); color: var(--text-secondary); } - -.status-badge { - display: inline-flex; - align-items: center; - gap: 6px; - font-family: var(--font-mono); - font-size: .7rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} -.status-badge .led { width: 6px; height: 6px; border-radius: 50%; } -.status-active .led { background: var(--accent); box-shadow: 0 0 6px var(--accent); } -.status-inactive .led { background: var(--text-dim); } - -/* ---------- Stat cards ---------- */ -.stat-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin-bottom: 28px; -} -.stat { - background: var(--bg-elev-1); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 18px; - position: relative; -} -.stat-label { - font-family: var(--font-mono); - font-size: .68rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--text-dim); -} -.stat-value { - font-family: var(--font-mono); - font-size: 2rem; - font-weight: 700; - color: var(--text-primary); - line-height: 1.1; - margin-top: 6px; -} -.stat-accent .stat-value { color: var(--accent); } -.stat-warn .stat-value { color: var(--amber); } - -/* ---------- QR / MFA pages ---------- */ -.mfa-qr { - display: block; - margin: 0 auto; - width: 200px; - height: 200px; - background: white; - padding: 12px; - border-radius: var(--radius-sm); - border: 1px solid var(--border); -} -.mfa-secret { - font-family: var(--font-mono); - font-size: .9rem; - background: var(--bg-elev-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 8px 12px; - user-select: all; - word-break: break-all; - text-align: center; -} - -/* ---------- Footer ---------- */ -.footer { - max-width: 1320px; - margin: 0 auto; - padding: 18px 24px 36px; - display: flex; - align-items: center; - gap: 10px; - font-family: var(--font-mono); - font-size: .75rem; - color: var(--text-dim); - border-top: 1px solid var(--border); -} -/* ---------- Avatar panel (account page) ---------- */ -.avatar-row { - display: flex; - gap: 24px; - align-items: flex-start; - flex-wrap: wrap; -} -.avatar-preview { - width: 96px; height: 96px; - border-radius: 50%; - overflow: hidden; - flex: 0 0 auto; - border: 1px solid var(--border-strong); - background: var(--bg-elev-3); - display: inline-flex; - align-items: center; - justify-content: center; -} -.avatar-preview img { width: 100%; height: 100%; object-fit: cover; display: block; } -.avatar-initials { - font-family: var(--font-mono); - font-size: 1.8rem; - font-weight: 700; - color: var(--accent); -} -.avatar-actions { flex: 1 1 260px; min-width: 260px; } - -/* ---------- Log export / email actions ---------- */ -.log-actions { - display: flex; - gap: 12px; - align-items: center; - flex-wrap: wrap; -} -.log-email-form { - display: inline-flex; - gap: 8px; - align-items: center; -} -.log-email-form input[type="text"], -.log-email-form input[type="email"] { margin: 0; min-width: 220px; } -@media (max-width: 720px) { - .log-actions { width: 100%; } - .log-email-form { width: 100%; } - .log-email-form input { flex: 1; min-width: 0; } -} - -.footer-sep { opacity: .5; } -.footer-copy { - margin-left: auto; - color: var(--text-secondary); - letter-spacing: .02em; -} -.footer .status-pill { margin-left: 18px; } -@media (max-width: 720px) { - .footer { flex-wrap: wrap; } - .footer-copy { margin-left: 0; flex-basis: 100%; order: 3; } - .footer .status-pill { margin-left: auto; } -} -.kbd { - background: var(--bg-elev-2); - border: 1px solid var(--border); - border-radius: 3px; - padding: 1px 6px; - font-size: .72rem; - color: var(--text-secondary); -} -.status-pill { - margin-left: auto; - display: inline-flex; - align-items: center; - gap: 6px; - text-transform: uppercase; -} -.status-pill .led { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--accent); - box-shadow: 0 0 6px var(--accent); - animation: led-pulse 2.5s ease-in-out infinite; -} -@keyframes led-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: .4; } -} - -/* ---------- Pagination ---------- */ -.pagination { - display: flex; - justify-content: center; - gap: 6px; - margin-top: 20px; - flex-wrap: wrap; -} -.pagination a, .pagination span { - font-family: var(--font-mono); - font-size: .82rem; - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-secondary); -} -.pagination a:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; } -.pagination .current { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); } - -/* ---------- Misc ---------- */ -.row-spread { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; } -.row-gap { display: flex; gap: 12px; flex-wrap: wrap; } -.muted { color: var(--text-secondary); } -.dim { color: var(--text-dim); } -hr { border: 0; border-top: 1px dashed var(--border); margin: 24px 0; } - -@media (max-width: 800px) { - .topbar-inner { grid-template-columns: 1fr; gap: 12px; } - .topbar-nav { justify-self: flex-start; flex-wrap: wrap; } - .topbar-right { justify-self: flex-start; } -} diff --git a/Alert Hub System/app/static/favicon.svg b/Alert Hub System/app/static/favicon.svg deleted file mode 100644 index 6dd6096..0000000 --- a/Alert Hub System/app/static/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Alert Hub System/app/static/js/app.js b/Alert Hub System/app/static/js/app.js deleted file mode 100644 index 1d6a756..0000000 --- a/Alert Hub System/app/static/js/app.js +++ /dev/null @@ -1,73 +0,0 @@ -// ====== Theme toggle ======================================================= -(function () { - const btn = document.getElementById('theme-toggle'); - if (!btn) return; - btn.addEventListener('click', () => { - const root = document.documentElement; - const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; - root.setAttribute('data-theme', next); - try { localStorage.setItem('alerthub-theme', next); } catch (_) {} - }); -})(); - -// ====== Countdown timer ==================================================== -// Each .countdown element has data-expires="YYYY-MM-DD" and renders 4 cells -// for days / hours / minutes / seconds. Updates every second. -(function () { - const els = document.querySelectorAll('.countdown[data-expires]'); - if (!els.length) return; - - function pad(n) { return n < 10 ? '0' + n : '' + n; } - - function render() { - const now = new Date(); - els.forEach((el) => { - const target = new Date(el.dataset.expires + 'T23:59:59'); - let diff = (target.getTime() - now.getTime()) / 1000; - const card = el.closest('.alert-card'); - const negative = diff < 0; - diff = Math.abs(diff); - - const days = Math.floor(diff / 86400); - const hours = Math.floor((diff % 86400) / 3600); - const minutes = Math.floor((diff % 3600) / 60); - const seconds = Math.floor(diff % 60); - - const segs = el.querySelectorAll('.countdown-value'); - if (segs.length === 4) { - segs[0].textContent = days; - segs[1].textContent = pad(hours); - segs[2].textContent = pad(minutes); - segs[3].textContent = pad(seconds); - } - - if (card) { - card.classList.remove('is-warning', 'is-critical', 'is-expired'); - if (negative) { - card.classList.add('is-expired'); - } else if (days < 7) { - card.classList.add('is-critical'); - } else if (days < 30) { - card.classList.add('is-warning'); - } - } - - const dotLabel = el.parentElement && el.parentElement.querySelector('.alert-state-text'); - if (dotLabel) { - if (negative) dotLabel.textContent = 'EXPIRED'; - else if (days < 7) dotLabel.textContent = 'CRITICAL'; - else if (days < 30) dotLabel.textContent = 'WARNING'; - else dotLabel.textContent = 'NOMINAL'; - } - }); - } - render(); - setInterval(render, 1000); -})(); - -// ====== Confirm-on-click for destructive actions =========================== -document.querySelectorAll('form[data-confirm]').forEach((form) => { - form.addEventListener('submit', (e) => { - if (!window.confirm(form.dataset.confirm)) e.preventDefault(); - }); -}); diff --git a/Alert Hub System/app/templates/account.html b/Alert Hub System/app/templates/account.html deleted file mode 100644 index faf373f..0000000 --- a/Alert Hub System/app/templates/account.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "base.html" %} -{% block title %}Account{% endblock %} -{% block content %} -
-
- -

{{ current_user.username }}

-

{{ current_user.email }} · role {{ current_user.role }}

-
-
- -
-

Profile avatar

-
-
-
- {% if current_user.has_avatar %} - Current avatar - {% else %} - {{ current_user.initials }} - {% endif %} -
-
-
- {{ avatar_form.csrf_token }} -
{{ avatar_form.avatar.label }}{{ avatar_form.avatar(accept="image/*") }}
-
PNG, JPG, GIF or WebP. Squared and resized to {{ 256 }}×{{ 256 }} automatically. Max 2 MB.
-
- {{ avatar_form.submit(class_="btn btn-primary") }} - {% if current_user.has_avatar %} - - {% endif %} -
-
- {% if current_user.has_avatar %} -
- -
- {% endif %} -
-
-
-
- -
-
-

Change password

-
-
- {{ password_form.csrf_token }} -
{{ password_form.current_password.label }}{{ password_form.current_password(autocomplete="current-password") }}
-
{{ password_form.new_password.label }}{{ password_form.new_password(autocomplete="new-password") }}
-
{{ password_form.confirm.label }}{{ password_form.confirm(autocomplete="new-password") }}
- {{ password_form.submit(class_="btn btn-primary") }} -
-
-
- -
-
-

Multi-factor authentication

- - {{ 'enabled' if current_user.mfa_enabled else 'disabled' }} - -
-
- {% if current_user.mfa_enabled %} -

MFA is currently active on your account. You'll be prompted for a 6-digit code on every sign-in.

-
- - -
- {% else %} -

MFA is currently not configured. Strongly recommended for admin accounts.

- Set up MFA now - {% endif %} -
-
-
- -{% endblock %} diff --git a/Alert Hub System/app/templates/alert_form.html b/Alert Hub System/app/templates/alert_form.html deleted file mode 100644 index a2b4fb2..0000000 --- a/Alert Hub System/app/templates/alert_form.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if alert else 'New' }} alert{% endblock %} -{% block content %} -
-
- -

{{ 'Edit alert' if alert else 'New alert' }}

- {% if alert %}

Last updated {{ alert.updated_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.

{% endif %} -
- ← Back -
- -
-
-
- {{ form.csrf_token }} -
- {{ form.title.label }}{{ form.title(placeholder="e.g. Microsoft 365 Renewal") }} - {% for err in form.title.errors %}
{{ err }}
{% endfor %} -
-
-
- {{ form.category.label }}{{ form.category(placeholder="License, SSL, Domain, Contract …") }} - {% for err in form.category.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.expiration_date.label }}{{ form.expiration_date() }} - {% for err in form.expiration_date.errors %}
{{ err }}
{% endfor %} -
-
-
- {{ form.description.label }}{{ form.description(rows=4, placeholder="Optional detail shown on the alert card.") }} - {% for err in form.description.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.reminder_days.label }}{{ form.reminder_days() }} -
Comma-separated days BEFORE expiration when an email reminder fires. Example: 30,14,7,1
- {% for err in form.reminder_days.errors %}
{{ err }}
{% endfor %} -
-
{{ form.is_active() }} {{ form.is_active.label }}
-
- {{ form.submit(class_="btn btn-primary") }} - Cancel -
-
-
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/alerts_list.html b/Alert Hub System/app/templates/alerts_list.html deleted file mode 100644 index e0e2ed2..0000000 --- a/Alert Hub System/app/templates/alerts_list.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends "base.html" %} -{% block title %}Manage Alerts{% endblock %} -{% block content %} -
-
- -

Manage alerts

-

Create, modify, or retire alerts. Only administrators can make changes.

-
- {% if current_user.is_admin %} - + New alert - {% endif %} -
- -{% if alerts %} - - - - - - - - - - - - - {% for a in alerts %} - - - - - - - - - {% endfor %} - -
TitleCategoryExpiresRemindersStatusActions
{{ a.title }}{% if a.description %}
{{ a.description[:80] }}{% if a.description|length > 80 %}…{% endif %}{% endif %}
{% if a.category %}{{ a.category }}{% else %}{% endif %}{{ a.expiration_date.isoformat() }}{{ a.reminder_days|join(', ') }}d - - {{ 'active' if a.is_active else 'inactive' }} - - - {% if current_user.is_admin %} - Edit -
- - -
-
- - -
- {% else %} - view only - {% endif %} -
-{% else %} -
-

No alerts yet.

- {% if current_user.is_admin %}Create one{% endif %} -
-{% endif %} -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/audit_log.html b/Alert Hub System/app/templates/backoffice/audit_log.html deleted file mode 100644 index c78f147..0000000 --- a/Alert Hub System/app/templates/backoffice/audit_log.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "base.html" %} -{% block title %}Audit log{% endblock %} -{% block content %} -
-
- -

Audit log

-

Sign-ins, configuration changes, and alert mutations. Most-recent first.

-
-
- ⬇ Export CSV -
- {{ log_email_form.csrf_token }} - {{ log_email_form.log_recipient(placeholder="destination@example.com") }} - {{ log_email_form.submit(class_="btn btn-primary") }} -
-
-
- - - - - - - {% for e in pagination.items %} - - - - - - - - {% else %} - - {% endfor %} - -
Timestamp (UTC)UserActionDetailsIP
{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ e.username or '—' }}{{ e.action }}{{ e.details or '' }}{{ e.ip_address or '' }}
No audit entries yet.
- -{% if pagination.pages > 1 %} - -{% endif %} -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/dashboard.html b/Alert Hub System/app/templates/backoffice/dashboard.html deleted file mode 100644 index 41b6226..0000000 --- a/Alert Hub System/app/templates/backoffice/dashboard.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "base.html" %} -{% block title %}Dashboard{% endblock %} -{% block content %} -
-
- -

Operations dashboard

-

System status as of {{ today.isoformat() }}.

-
-
- -
-
-
Active alerts
-
{{ active_alerts }}
-
-
-
Users
-
{{ user_count }}
-
-
-
Administrators
-
{{ admin_count }}
-
-
- -
-
-
-

Expiring soonest

- settings → -
-
- {% if expiring_soon %} - - - - - - {% for a in expiring_soon %} - {% set days = (a.expiration_date - today).days %} - - - - - - {% endfor %} - -
TitleExpiresDays
{{ a.title }}{% if a.category %} {{ a.category }}{% endif %}{{ a.expiration_date.isoformat() }}{{ days }}d
- {% else %} -

No upcoming alerts.

- {% endif %} -
-
- -
-

Recent activity

- full log → -
-
- {% if recent_audit %} -
    - {% for entry in recent_audit %} -
  • - {{ entry.action }} {% if entry.username %}· {{ entry.username }}{% endif %}{% if entry.details %}
    {{ entry.details }}{% endif %}
    - {{ entry.timestamp.strftime('%m-%d %H:%M') }} -
  • - {% endfor %} -
- {% else %} -

No activity yet.

- {% endif %} -
-
-
- -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/mail_config.html b/Alert Hub System/app/templates/backoffice/mail_config.html deleted file mode 100644 index 14ef9c0..0000000 --- a/Alert Hub System/app/templates/backoffice/mail_config.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail configuration{% endblock %} -{% block content %} -
-
- -

SMTP configuration

-

External mail server used to dispatch alert reminders. Edits take effect immediately.

-
-
- -
-
-

Server settings

-
-
- {{ form.csrf_token }} -
-
- {{ form.smtp_host.label }}{{ form.smtp_host(placeholder="smtp.example.com") }} - {% for err in form.smtp_host.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.smtp_port.label }}{{ form.smtp_port() }} - {% for err in form.smtp_port.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.smtp_encryption.label }}{{ form.smtp_encryption() }} -
587 → STARTTLS · 465 → SSL/TLS · 25 → none
-
-
- {{ form.smtp_username.label }}{{ form.smtp_username(autocomplete="off") }} -
-
- {{ form.smtp_password.label }}{{ form.smtp_password(autocomplete="new-password") }} -
Leave blank to keep the current password.
-
-
- {{ form.smtp_from_address.label }}{{ form.smtp_from_address() }} -
-
- {{ form.smtp_from_name.label }}{{ form.smtp_from_name() }} -
-
- -
-

Sent-folder copy (IMAP)

-

When enabled, a copy of every message the platform sends is saved to the mailbox's Sent folder over IMAP, so it appears in your normal mail client.

-
{{ form.imap_enabled() }} {{ form.imap_enabled.label }}
-
-
- {{ form.imap_host.label }}{{ form.imap_host(placeholder="imap.purelymail.com") }} - {% for err in form.imap_host.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.imap_port.label }}{{ form.imap_port(placeholder="993") }} - {% for err in form.imap_port.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.imap_sent_folder.label }}{{ form.imap_sent_folder(placeholder="Sent") }} -
Usually Sent.
-
-
- {{ form.imap_username.label }}{{ form.imap_username(autocomplete="off") }} -
-
- {{ form.imap_password.label }}{{ form.imap_password(autocomplete="new-password") }} -
Blank keeps current; if never set, the SMTP password is used.
-
-
- - {{ form.submit(class_="btn btn-primary") }} -
-
-
- -
-

Send test email

-
-

Confirms credentials, port, and encryption are correct.

-
- {{ test_form.csrf_token }} -
{{ test_form.test_recipient.label }}{{ test_form.test_recipient(placeholder="you@example.com") }}
- {{ test_form.submit(class_="btn btn-ghost btn-block") }} -
-
-
-
- -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/mail_log.html b/Alert Hub System/app/templates/backoffice/mail_log.html deleted file mode 100644 index 3370c4b..0000000 --- a/Alert Hub System/app/templates/backoffice/mail_log.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail log{% endblock %} -{% block content %} -
-
- -

Mail log

-

Every message the platform has dispatched, whether a copy reached the Sent folder, and its delivery result. Most-recent first.

-
-
- ⬇ Export CSV -
- {{ log_email_form.csrf_token }} - {{ log_email_form.log_recipient(placeholder="destination@example.com") }} - {{ log_email_form.submit(class_="btn btn-primary") }} -
-
-
- - - - - - - - - - {% for m in pagination.items %} - - - - - - - - - - - {% else %} - - {% endfor %} - -
Timestamp (UTC)KindRecipientsSubjectStatusSent copyByInfo
{{ m.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ m.kind }}{{ m.recipients or '—' }}{{ m.subject or '—' }} - {% if m.status == 'sent' %} - sent - {% else %} - failed - {% endif %} - - {% if m.sent_copy == 'saved' %} - saved - {% elif m.sent_copy == 'failed' %} - failed - {% elif m.sent_copy == 'disabled' %} - disabled - {% else %} - - {% endif %} - {{ m.triggered_by or 'system' }}{{ m.info or '' }}
No mail has been sent yet.
- -{% if pagination.pages > 1 %} - -{% endif %} -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/settings.html b/Alert Hub System/app/templates/backoffice/settings.html deleted file mode 100644 index f637740..0000000 --- a/Alert Hub System/app/templates/backoffice/settings.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "base.html" %} -{% block title %}Settings{% endblock %} -{% block content %} -
-
- -

Global settings

-

Default reminder cadence and scheduler controls.

-
-
- -
-
-

Default reminder schedule

-
-
- {{ form.csrf_token }} -
- {{ form.default_reminder_days.label }}{{ form.default_reminder_days() }} -
When creating a new alert, this is the default. Existing alerts are unaffected.
- {% for err in form.default_reminder_days.errors %}
{{ err }}
{% endfor %} -
- {{ form.submit(class_="btn btn-primary") }} -
-
-
- -
-

Scheduler

-
-

Reminder dispatch runs every {{ scheduler_interval }} minute(s).

-

Configured via the SCHEDULER_INTERVAL_MINUTES environment variable. Restart the container to change it.

- -
- -

You can trigger a check immediately to verify reminders dispatch correctly.

-
- - -
-
-
-
- -{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/user_form.html b/Alert Hub System/app/templates/backoffice/user_form.html deleted file mode 100644 index ee90e1a..0000000 --- a/Alert Hub System/app/templates/backoffice/user_form.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if user else 'New' }} user{% endblock %} -{% block content %} -
-
- -

{{ 'Edit user' if user else 'New user' }}

- {% if user %}

Created {{ user.created_at.strftime('%Y-%m-%d') }}

{% endif %} -
- ← Back -
- -
-
-
- {{ form.csrf_token }} -
-
{{ form.username.label }}{{ form.username() }} - {% for err in form.username.errors %}
{{ err }}
{% endfor %} -
-
{{ form.email.label }}{{ form.email() }} - {% for err in form.email.errors %}
{{ err }}
{% endfor %} -
-
-
-
{{ form.role.label }}{{ form.role() }}
-
{{ form.is_active() }} {{ form.is_active.label }}
-
-
- {{ form.password.label }}{{ form.password(autocomplete="new-password") }} -
{% if user %}Leave blank to keep the existing password.{% else %}Min {{ config.PASSWORD_MIN_LENGTH }} characters.{% endif %}
- {% for err in form.password.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.submit(class_="btn btn-primary") }} - Cancel -
-
-
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/backoffice/users_list.html b/Alert Hub System/app/templates/backoffice/users_list.html deleted file mode 100644 index 48a7e76..0000000 --- a/Alert Hub System/app/templates/backoffice/users_list.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users{% endblock %} -{% block content %} -
-
- -

Users

-

Manage accounts, roles, MFA, and lockouts.

-
- + New user -
- - - - - - - - - {% for u in users %} - - - - - - - - - - {% endfor %} - -
UsernameEmailRoleMFAStatusLast loginActions
{{ u.username }}{% if u.id == current_user.id %} (you){% endif %}{{ u.email }}{{ u.role }} - - {{ 'on' if u.mfa_enabled else 'off' }} - - - {% if u.is_locked %} - locked - {% else %} - {{ 'active' if u.is_active else 'disabled' }} - {% endif %} - - {% if u.last_login_at %}{{ u.last_login_at.strftime('%Y-%m-%d %H:%M') }}{% else %}never{% endif %} - - Edit - {% if u.mfa_enabled %} -
- - -
- {% endif %} - {% if u.is_locked %} -
- - -
- {% endif %} - {% if u.id != current_user.id %} -
- - -
- {% endif %} -
-{% endblock %} diff --git a/Alert Hub System/app/templates/base.html b/Alert Hub System/app/templates/base.html deleted file mode 100644 index 3b74e3b..0000000 --- a/Alert Hub System/app/templates/base.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - {% block title %}{{ brand_name }}{% endblock %} — {{ brand_name }}{% if is_backoffice %} · backoffice{% endif %} - - - - - - - - -
-
- - ▮▮ - {{ brand_name }} - {{ 'BACKOFFICE' if is_backoffice else 'FRONTOFFICE' }} - - - {% if current_user.is_authenticated and not session.get('mfa_pending') %} - - - - {% else %} -
- -
- {% endif %} -
-
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for cat, msg in messages %} -
- - {{ msg }} -
- {% endfor %} -
- {% endif %} - {% endwith %} - - {% block content %}{% endblock %} -
- - {% if request.endpoint != 'auth.login' %} -
- {{ brand_name }} - · - {{ 'admin console' if is_backoffice else 'alert console' }} - {{ footer_text }} - operational -
- {% endif %} - - - {% block scripts %}{% endblock %} - - diff --git a/Alert Hub System/app/templates/error.html b/Alert Hub System/app/templates/error.html deleted file mode 100644 index d964f8d..0000000 --- a/Alert Hub System/app/templates/error.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ code }} · Error{% endblock %} -{% block content %} -
-
-
error
-

{{ code }}

-

{{ message }}

- ← Return home -
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/index.html b/Alert Hub System/app/templates/index.html deleted file mode 100644 index 555e91f..0000000 --- a/Alert Hub System/app/templates/index.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "base.html" %} -{% block title %}View · Alerts{% endblock %} -{% block content %} -
-
- -

Active alerts

-

Live countdown to expiration. Reminders are dispatched automatically by email per the configured schedule.

-
-
- today: {{ today_iso }} - {% if current_user.is_admin %} - + New alert - {% endif %} -
-
- -{% if alerts %} -
- {% for a in alerts %} - {% set state = 'is-expired' if a.days_left < 0 else ('is-critical' if a.days_left < 7 else ('is-warning' if a.days_left < 30 else '')) %} - {% set state_text = 'EXPIRED' if a.days_left < 0 else ('CRITICAL' if a.days_left < 7 else ('WARNING' if a.days_left < 30 else 'NOMINAL')) %} -
-
-

{{ a.title }}

- {% if a.category %}{{ a.category }}{% endif %} -
- {% if a.description %}

{{ a.description }}

{% endif %} - -
- {{ a.title }} · expires {{ a.expiration_date }} -
- -
-
days
-
hours
-
min
-
sec
-
- -
- {{ state_text }} - reminders: {{ a.reminder_days|join(', ') }}d -
-
- {% endfor %} -
-{% else %} -
-
▮▮
-

No active alerts.

- {% if current_user.is_admin %} -

Add your first one — license renewals, certificates, contracts, …

- Create the first alert - {% else %} -

Ask an administrator to create alerts for your team.

- {% endif %} -
-{% endif %} -{% endblock %} diff --git a/Alert Hub System/app/templates/login.html b/Alert Hub System/app/templates/login.html deleted file mode 100644 index 776e0ee..0000000 --- a/Alert Hub System/app/templates/login.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in{% endblock %} -{% block content %} -
-
-
authenticate
-

Sign in to {{ brand_name }}

-

{% if is_backoffice %}Administrator console.{% else %}Alert operations console.{% endif %}

-
- {{ form.csrf_token }} -
- {{ form.username.label }} - {{ form.username(autocomplete="username", autofocus=True) }} -
-
- {{ form.password.label }} - {{ form.password(autocomplete="current-password") }} -
- {{ form.submit(class_="btn btn-primary btn-block") }} -
-
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/mfa.html b/Alert Hub System/app/templates/mfa.html deleted file mode 100644 index c2275e2..0000000 --- a/Alert Hub System/app/templates/mfa.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} -{% block title %}Verify identity{% endblock %} -{% block content %} -
-
-
verify
-

Two-factor authentication

-

Enter the 6-digit code from your authenticator app.

-
- {{ form.csrf_token }} -
- {{ form.code.label }} - {{ form.code(autocomplete="one-time-code", inputmode="numeric", pattern="[0-9]{6}", autofocus=True, maxlength=6) }} -
- {{ form.submit(class_="btn btn-primary btn-block") }} -
-
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/mfa_setup.html b/Alert Hub System/app/templates/mfa_setup.html deleted file mode 100644 index 7c07a59..0000000 --- a/Alert Hub System/app/templates/mfa_setup.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base.html" %} -{% block title %}Set up MFA{% endblock %} -{% block content %} -
-
- -

Enable multi-factor authentication

-

Scan the QR code with your authenticator (Google Authenticator, Authy, 1Password, …) and enter the 6-digit code to confirm.

-
-
- -
-
- MFA QR code -

Manual key (if you can't scan):

-
{{ secret }}
- -
- -
- {{ form.csrf_token }} -
- {{ form.code.label }} - {{ form.code(autocomplete="one-time-code", inputmode="numeric", pattern="[0-9]{6}", maxlength=6) }} -
Enter the current 6-digit code shown in your authenticator.
-
-
- {{ form.submit(class_="btn btn-primary") }} - Cancel -
-
-
-
-{% endblock %} diff --git a/Alert Hub System/app/templates/setup.html b/Alert Hub System/app/templates/setup.html deleted file mode 100644 index dffc1c5..0000000 --- a/Alert Hub System/app/templates/setup.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "base.html" %} -{% block title %}Initial setup{% endblock %} -{% block content %} -
-
-
first-run setup
-

Create administrator

-

No accounts exist yet. Create the first administrator to secure {{ brand_name }}.

-
- {{ form.csrf_token }} -
- {{ form.username.label }} - {{ form.username(autocomplete="username", autofocus=True) }} - {% for err in form.username.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.email.label }} - {{ form.email(autocomplete="email") }} - {% for err in form.email.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.password.label }} - {{ form.password(autocomplete="new-password") }} - {% for err in form.password.errors %}
{{ err }}
{% endfor %} -
-
- {{ form.confirm.label }} - {{ form.confirm(autocomplete="new-password") }} - {% for err in form.confirm.errors %}
{{ err }}
{% endfor %} -
- {{ form.submit(class_="btn btn-primary btn-block") }} -
-
-
-{% endblock %} diff --git a/Alert Hub System/docker-compose.yml b/Alert Hub System/docker-compose.yml deleted file mode 100644 index e7cbae8..0000000 --- a/Alert Hub System/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -# docker-compose.yml -# All runtime configuration is read from the .env file in the same folder. -# This file intentionally contains NO secrets and NO environment values. - -services: - alerthub: - build: - context: . - dockerfile: Dockerfile - image: alerthub:latest - container_name: alerthub - restart: unless-stopped - env_file: - - .env - ports: - - "${FRONTOFFICE_PORT}:${FRONTOFFICE_PORT}" - volumes: - - alerthub_data:/data - # Hardening - read_only: false # SQLite needs to write to /data; the rest of FS is owned by non-root user - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:${FRONTOFFICE_PORT}/login"] - interval: 30s - timeout: 5s - start_period: 15s - retries: 3 - -volumes: - alerthub_data: - name: alerthub_data diff --git a/Alert Hub System/entrypoint.sh b/Alert Hub System/entrypoint.sh deleted file mode 100644 index 16727fe..0000000 --- a/Alert Hub System/entrypoint.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -# Starts the unified AlertHub app (frontoffice + /backoffice) on a single port. -# POSIX /bin/sh (dash) compatible. Uses exec so Gunicorn becomes the container's -# main process and receives signals directly for clean, graceful shutdown. - -set -eu - -PORT="${APP_PORT:-${FRONTOFFICE_PORT:-8080}}" -# A single worker guarantees exactly one reminder scheduler. Concurrency is -# handled with threads, which is plenty for this internal tool. You can raise -# GUNICORN_THREADS if needed; leave GUNICORN_WORKERS at 1 to avoid running the -# scheduler more than once. -WORKERS="${GUNICORN_WORKERS:-1}" -THREADS="${GUNICORN_THREADS:-4}" - -echo ">> AlertHub starting (frontoffice + /backoffice on one port)" -echo " listening -> 0.0.0.0:${PORT}" - -exec gunicorn --bind "0.0.0.0:${PORT}" \ - --workers "${WORKERS}" \ - --threads "${THREADS}" \ - --timeout 60 \ - --graceful-timeout 30 \ - --access-logfile - \ - --error-logfile - \ - "wsgi:application" diff --git a/Alert Hub System/requirements.txt b/Alert Hub System/requirements.txt deleted file mode 100644 index 7360340..0000000 --- a/Alert Hub System/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -Flask==3.1.3 -Flask-Login==0.6.3 -Flask-WTF==1.2.2 -WTForms==3.2.1 -SQLAlchemy==2.0.36 -Flask-SQLAlchemy==3.1.1 -bcrypt==4.2.1 -pyotp==2.9.0 -qrcode[pil]==8.0 -Pillow==11.1.0 -APScheduler==3.11.0 -python-dotenv==1.0.1 -gunicorn==23.0.0 -email-validator==2.2.0 diff --git a/Alert Hub System/wsgi.py b/Alert Hub System/wsgi.py deleted file mode 100644 index ee21a41..0000000 --- a/Alert Hub System/wsgi.py +++ /dev/null @@ -1,13 +0,0 @@ -"""WSGI entry point for AlertHub. - -Serves the whole application — the frontoffice at the root and the backoffice -under /backoffice — on a single port. -""" -from app import create_app - -application = create_app() - -if __name__ == "__main__": - import os - port = int(os.environ.get("APP_PORT", os.environ.get("FRONTOFFICE_PORT", "8080"))) - application.run(host="0.0.0.0", port=port, debug=False) diff --git a/Alert Hub System/wsgi_backoffice.py b/Alert Hub System/wsgi_backoffice.py deleted file mode 100644 index e31005a..0000000 --- a/Alert Hub System/wsgi_backoffice.py +++ /dev/null @@ -1,9 +0,0 @@ -"""WSGI entry point for the backoffice (port 9001).""" -from app import create_app - -application = create_app("backoffice") - -if __name__ == "__main__": - import os - port = int(os.environ.get("BACKOFFICE_PORT", "9001")) - application.run(host="0.0.0.0", port=port, debug=False) diff --git a/Alert Hub System/wsgi_frontoffice.py b/Alert Hub System/wsgi_frontoffice.py deleted file mode 100644 index cdc51cd..0000000 --- a/Alert Hub System/wsgi_frontoffice.py +++ /dev/null @@ -1,9 +0,0 @@ -"""WSGI entry point for the frontoffice (port 8080).""" -from app import create_app - -application = create_app("frontoffice") - -if __name__ == "__main__": - import os - port = int(os.environ.get("FRONTOFFICE_PORT", "8080")) - application.run(host="0.0.0.0", port=port, debug=False) diff --git a/Approval Workflow/.env.example b/Approval Workflow/.env.example deleted file mode 100644 index 51b61a3..0000000 --- a/Approval Workflow/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: Docker Compose treats '$' in this file as variable substitution. -# If any value (e.g. a password) contains a literal '$', write it as '$$': -# MAIL_PASSWORD=xK9$$vh4pQ2 -> the container receives xK9$vh4pQ2 -# Otherwise you'll see a warning like: The "..." variable is not set. - -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/Dockerfile b/Approval Workflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow.zip deleted file mode 100644 index c6cf082..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/.env.example deleted file mode 100644 index ed07a41..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Inbound email (IMAP) — leave IMAP_HOST empty to disable email intake --- -IMAP_HOST=imap.example.com -IMAP_USER=inbox@example.com -IMAP_PASSWORD=secret -IMAP_FOLDER=INBOX -IMAP_POLL_SECONDS=30 - -# --- Outbound email (SMTP) — leave SMTP_HOST empty to print mail to console (dev) --- -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_SECURITY=starttls # starttls | ssl | none -SMTP_USER=inbox@example.com -SMTP_PASSWORD=secret -SMTP_FROM=approvals@example.com diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/README.md deleted file mode 100644 index 44ab993..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests arrive by email (or through a login-protected web form), get redirected to an approver, and once approved or denied, **everyone involved up to that point** is notified by email. Every request keeps a full audit log of everything that happened to it. - -## What it does - -- **Email intake.** An IMAP poller watches a mailbox. When a message arrives from a workflow's allowed domain, a request is created and redirected to that workflow's approver with one-click **Approve** / **Deny** links. Mail from any other domain is rejected (and logged). -- **Decisions notify everyone.** On approve or deny, the requester, anyone CC'd on the original email, and the approver all receive the outcome email. -- **Full log.** Every request records: received, matched workflow, approval requested, decision, notifications sent, and any mail errors — each with a timestamp. -- **Multiple named workflows**, each personalized: its own allowed sender domain, approver, and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, IMAP_*, SMTP_* -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `SMTP_HOST` empty prints outgoing mail to the console and leaving `IMAP_HOST` empty disables email intake — handy for trying the app before wiring up a mailbox. - -## How email intake targets a workflow - -1. **Plus-addressing:** mail sent to `inbox+@your-domain` targets that workflow. -2. **Subject tag:** a subject containing `[]`. -3. **Fallback:** the first active workflow whose allowed domain matches the sender. - -Whichever way a workflow is matched, the sender's domain must equal the workflow's allowed domain or the request is marked **rejected** and logged. - -## The request lifecycle - -``` -email / web form - │ received + logged - ▼ - approval request → approver (approve/deny links) - │ logged - ▼ - approver clicks a link (or admin decides in the UI) - │ decision logged - ▼ - APPROVED or DENIED notice → everyone involved so far - (requester + CCs + approver) logged -``` - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{decided_by}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit-log helpers -pipeline.py approval request + decision/notification logic -poller.py IMAP inbox watcher (background thread) -mailer.py SMTP sending + template rendering -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/app.py deleted file mode 100644 index 110a2dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,284 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -import poller -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not subject or not body: - flash("Subject and message are both required.", "error") - else: - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - requester = user["email"] or user["username"] - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.source='web' AND (r.requester=? OR r.requester=?) ORDER BY r.id DESC LIMIT 15", - (session["username"], - (g.db.execute("SELECT email FROM users WHERE id=?", (session["user_id"],)).fetchone()["email"] or "-"), - )).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests) - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approver_email FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or req["approver_email"] - status, req2, message = pipeline.decide(token, action, decided_by) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)") - return redirect(url_for("admin_request", rid=rid)) - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - return render_template("request_detail.html", req=req, logs=logs, participants=participants) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - allowed_domain = f.get("allowed_domain", "").strip().lstrip("@").lower() - approver_email = f.get("approver_email", "").strip() - if not (name and allowed_domain and approver_email): - flash("Name, allowed domain and approver email are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, allowed_domain, approver_email, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), allowed_domain, - approver_email, 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created.", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, allowed_domain=?," - " approver_email=?, active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), allowed_domain, - approver_email, 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() -poller.start() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/db.py deleted file mode 100644 index 86604ae..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,157 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - allowed_domain TEXT NOT NULL, -- only senders @this domain are accepted - approver_email TEXT NOT NULL, -- who receives the approval request - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL, -- 'email' or 'web' - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied / rejected - token TEXT UNIQUE NOT NULL, -- secret for approve/deny links - decided_by TEXT DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed: {subject}", - "approval_body": ( - "A new request needs your decision.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - return con - - -def init_db(): - con = get_db() - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/mailer.py deleted file mode 100644 index e0da6b3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Outbound email (SMTP) and template rendering.""" -import os -import smtplib -from email.message import EmailMessage - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def send_mail(to_addrs, subject, body): - """Send a plain-text email. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - host = os.environ.get("SMTP_HOST", "") - port = int(os.environ.get("SMTP_PORT", "587")) - user = os.environ.get("SMTP_USER", "") - password = os.environ.get("SMTP_PASSWORD", "") - sender = os.environ.get("SMTP_FROM", user or "approvalflow@localhost") - security = os.environ.get("SMTP_SECURITY", "starttls").lower() # starttls / ssl / none - - msg = EmailMessage() - msg["From"] = sender - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not host: - # Dev mode: print instead of sending so the app is testable without SMTP. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - return True, "dev mode (SMTP_HOST not set) — printed to console" - - try: - if security == "ssl": - server = smtplib.SMTP_SSL(host, port, timeout=30) - else: - server = smtplib.SMTP(host, port, timeout=30) - if security == "starttls": - server.starttls() - if user: - server.login(user, password) - server.send_message(msg) - server.quit() - return True, f"sent to {', '.join(to_addrs)}" - except Exception as e: - return False, f"SMTP error: {e}" diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 3ac20b4..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Request pipeline shared by the web form and the email poller.""" -import os - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def _vars(wf, req, **extra): - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def send_approval_request(request_id): - """Redirect the request to the approver, asking for approval.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - v = _vars(wf, req) - ok, detail = send_mail(wf["approver_email"], - render(wf["approval_subject"], **v), - render(wf["approval_body"], **v)) - db.add_participants(con, request_id, [wf["approver_email"]]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Approval request to {wf['approver_email']} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def decide(token, action, decided_by): - """Approve or deny a request; notify everyone involved so far. - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - new_status = "approved" if action == "approve" else "denied" - con.execute("UPDATE requests SET status=?, decided_by=?, decided_at=? WHERE id=?", - (new_status, decided_by, db.now(), req["id"])) - db.log(con, req["id"], new_status, f"Decision by {decided_by}") - - participants = db.add_participants(con, req["id"], [decided_by]) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper()) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, f"Request #{req['id']} {new_status}. Everyone involved has been notified." - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/poller.py b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/poller.py deleted file mode 100644 index 98df3cf..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/poller.py +++ /dev/null @@ -1,146 +0,0 @@ -"""IMAP inbox poller. - -Watches a mailbox for new messages and turns them into approval requests. - -Workflow matching, in order: - 1. Plus-addressing: mail sent to inbox+@domain targets that workflow. - 2. Subject tag: a subject containing [] targets that workflow. - 3. Fallback: the first active workflow whose allowed_domain matches the sender. - -The sender's domain must match the matched workflow's allowed_domain, -otherwise the message is logged and rejected. -""" -import email -import email.utils -import imaplib -import os -import re -import threading -import time -import traceback - -import db -import pipeline - - -def _addr_list(msg, header): - raw = msg.get_all(header, []) - return [a.lower() for _, a in email.utils.getaddresses(raw) if a] - - -def _body_text(msg): - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain" and not part.get("Content-Disposition"): - try: - return part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8", "replace") - except Exception: - continue - return "(no readable text body)" - payload = msg.get_payload(decode=True) - if payload is None: - return str(msg.get_payload()) - return payload.decode(msg.get_content_charset() or "utf-8", "replace") - - -def _match_workflow(con, sender, recipients, subject): - workflows = con.execute("SELECT * FROM workflows WHERE active=1").fetchall() - by_slug = {w["slug"]: w for w in workflows} - - # 1. plus-addressing: inbox+slug@... - for addr in recipients: - m = re.match(r"^[^+@]+\+([a-z0-9\-]+)@", addr) - if m and m.group(1) in by_slug: - return by_slug[m.group(1)], "plus-address" - - # 2. subject tag [slug] - for slug, w in by_slug.items(): - if f"[{slug}]" in subject.lower(): - return w, "subject tag" - - # 3. sender-domain fallback - domain = sender.split("@")[-1].lower() if "@" in sender else "" - for w in workflows: - if w["allowed_domain"].lower().lstrip("@") == domain: - return w, "sender domain" - return None, None - - -def process_message(raw_bytes): - msg = email.message_from_bytes(raw_bytes) - sender = (email.utils.parseaddr(msg.get("From", ""))[1] or "").lower() - recipients = _addr_list(msg, "To") + _addr_list(msg, "Cc") - subject = msg.get("Subject", "(no subject)") - body = _body_text(msg).strip() - - con = db.get_db() - try: - wf, how = _match_workflow(con, sender, recipients, subject) - if wf is None: - print(f"[poller] No workflow matched mail from {sender} — ignored.") - return - - sender_domain = sender.split("@")[-1] if "@" in sender else "" - allowed = wf["allowed_domain"].lower().lstrip("@") - if sender_domain != allowed: - rid, _ = db.create_request(con, wf["id"], "email", sender, subject, body) - con.execute("UPDATE requests SET status='rejected' WHERE id=?", (rid,)) - db.log(con, rid, "rejected", - f"Sender domain '{sender_domain}' is not the allowed domain '@{allowed}' for workflow '{wf['name']}'") - con.commit() - print(f"[poller] Rejected mail from {sender} for workflow {wf['name']} (wrong domain).") - return - - # CC'd people are "involved up to this point" too — everyone on To/Cc - # except the sender and the intake mailbox itself (incl. its +tag variants) - inbox_local = (os.environ.get("IMAP_USER") or "").split("@")[0].lower() - - def is_intake(addr): - base = addr.split("@")[0].split("+")[0] - return (inbox_local and base == inbox_local) or \ - addr.split("@")[0].endswith("+" + wf["slug"]) - - ccs = [a for a in recipients if a != sender and not is_intake(a)] - rid, token = db.create_request(con, wf["id"], "email", sender, subject, body, extra_participants=ccs) - db.log(con, rid, "matched", f"Matched workflow '{wf['name']}' via {how}") - con.commit() - - pipeline.send_approval_request(rid) - print(f"[poller] Request #{rid} created for workflow '{wf['name']}' from {sender}.") - except Exception: - traceback.print_exc() - finally: - con.close() - - -def poll_loop(): - host = os.environ.get("IMAP_HOST", "") - user = os.environ.get("IMAP_USER", "") - password = os.environ.get("IMAP_PASSWORD", "") - folder = os.environ.get("IMAP_FOLDER", "INBOX") - interval = int(os.environ.get("IMAP_POLL_SECONDS", "30")) - - if not host: - print("[poller] IMAP_HOST not set — email intake disabled (web form still works).") - return - - print(f"[poller] Watching {user} on {host} every {interval}s.") - while True: - try: - imap = imaplib.IMAP4_SSL(host) - imap.login(user, password) - imap.select(folder) - _, data = imap.search(None, "UNSEEN") - for num in data[0].split(): - _, msg_data = imap.fetch(num, "(RFC822)") - process_message(msg_data[0][1]) - imap.store(num, "+FLAGS", "\\Seen") - imap.logout() - except Exception as e: - print(f"[poller] Error: {e}") - time.sleep(interval) - - -def start(): - t = threading.Thread(target=poll_loop, daemon=True) - t.start() diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/static/style.css deleted file mode 100644 index f71bca8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied, .stamp.rejected { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 414608f..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
{{ stats.get('rejected', 0) }}Rejected
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - - {% else %} - - {% endfor %} -
NameIntake tagAllowed domainApproverStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
+{{ w.slug }} / [{{ w.slug }}]@{{ w.allowed_domain }}{{ w.approver_email }}{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectSourceStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.source }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 7fc20f3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 5b5579c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 9978c99..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index ed4950d..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · via {{ req.source }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Decided by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'pending' %} -
- - - Deciding here notifies everyone involved, same as the email links. -
- {% endif %} -
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index f62a879..0000000 --- a/Approval Workflow/OLD_VERSIONS/v1.0/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - {% if wf %}

Email intake: send to inbox+{{ wf.slug }}@your-domain or put [{{ wf.slug }}] in the subject.

{% endif %} -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {decided_by} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow.zip deleted file mode 100644 index ea8aeb5..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/.env.example deleted file mode 100644 index ed07a41..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Inbound email (IMAP) — leave IMAP_HOST empty to disable email intake --- -IMAP_HOST=imap.example.com -IMAP_USER=inbox@example.com -IMAP_PASSWORD=secret -IMAP_FOLDER=INBOX -IMAP_POLL_SECONDS=30 - -# --- Outbound email (SMTP) — leave SMTP_HOST empty to print mail to console (dev) --- -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_SECURITY=starttls # starttls | ssl | none -SMTP_USER=inbox@example.com -SMTP_PASSWORD=secret -SMTP_FROM=approvals@example.com diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/README.md deleted file mode 100644 index c6d92f7..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests arrive by email (or through a login-protected web form), get redirected to an approver, and once approved or denied, **everyone involved up to that point** is notified by email. Every request keeps a full audit log of everything that happened to it. - -## What it does - -- **Email intake.** An IMAP poller watches a mailbox. Incoming messages become requests and enter the workflow's **approval chain** with one-click **Approve** / **Deny** links. -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester, anyone CC'd on the original email, and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, matched workflow, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, IMAP_*, SMTP_* -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `SMTP_HOST` empty prints outgoing mail to the console and leaving `IMAP_HOST` empty disables email intake — handy for trying the app before wiring up a mailbox. - -## How email intake targets a workflow - -1. **Plus-addressing:** mail sent to `inbox+@your-domain` targets that workflow. -2. **Subject tag:** a subject containing `[]`. -3. **Fallback:** if exactly one active workflow exists, it is used. - -Unmatched mail is ignored. - -## The request lifecycle - -``` -email / web form - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + CCs + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit-log helpers -pipeline.py approval request + decision/notification logic -poller.py IMAP inbox watcher (background thread) -mailer.py SMTP sending + template rendering -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/app.py deleted file mode 100644 index 4df65e7..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,298 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -import poller -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not subject or not body: - flash("Subject and message are both required.", "error") - else: - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - requester = user["email"] or user["username"] - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.source='web' AND (r.requester=? OR r.requester=?) ORDER BY r.id DESC LIMIT 15", - (session["username"], - (g.db.execute("SELECT email FROM users WHERE id=?", (session["user_id"],)).fetchone()["email"] or "-"), - )).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests) - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - status, req2, message = pipeline.decide(token, action, decided_by) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)") - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() -poller.start() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/db.py deleted file mode 100644 index 1a9582c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,176 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL, -- 'email' or 'web' - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/mailer.py deleted file mode 100644 index e0da6b3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Outbound email (SMTP) and template rendering.""" -import os -import smtplib -from email.message import EmailMessage - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def send_mail(to_addrs, subject, body): - """Send a plain-text email. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - host = os.environ.get("SMTP_HOST", "") - port = int(os.environ.get("SMTP_PORT", "587")) - user = os.environ.get("SMTP_USER", "") - password = os.environ.get("SMTP_PASSWORD", "") - sender = os.environ.get("SMTP_FROM", user or "approvalflow@localhost") - security = os.environ.get("SMTP_SECURITY", "starttls").lower() # starttls / ssl / none - - msg = EmailMessage() - msg["From"] = sender - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not host: - # Dev mode: print instead of sending so the app is testable without SMTP. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - return True, "dev mode (SMTP_HOST not set) — printed to console" - - try: - if security == "ssl": - server = smtplib.SMTP_SSL(host, port, timeout=30) - else: - server = smtplib.SMTP(host, port, timeout=30) - if security == "starttls": - server.starttls() - if user: - server.login(user, password) - server.send_message(msg) - server.quit() - return True, f"sent to {', '.join(to_addrs)}" - except Exception as e: - return False, f"SMTP error: {e}" diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index c5ba954..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Request pipeline shared by the web form and the email poller. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - v = _vars(wf, req) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(wf["approval_body"], **v)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper()) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by): - """Handle a decision at the request's current step. - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/poller.py b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/poller.py deleted file mode 100644 index a9da7b5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/poller.py +++ /dev/null @@ -1,132 +0,0 @@ -"""IMAP inbox poller. - -Watches a mailbox for new messages and turns them into approval requests. - -Workflow matching, in order: - 1. Plus-addressing: mail sent to inbox+@domain targets that workflow. - 2. Subject tag: a subject containing [] targets that workflow. - 3. Fallback: if exactly one active workflow exists, it is used. - -Unmatched mail is ignored. -""" -import email -import email.utils -import imaplib -import os -import re -import threading -import time -import traceback - -import db -import pipeline - - -def _addr_list(msg, header): - raw = msg.get_all(header, []) - return [a.lower() for _, a in email.utils.getaddresses(raw) if a] - - -def _body_text(msg): - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain" and not part.get("Content-Disposition"): - try: - return part.get_payload(decode=True).decode(part.get_content_charset() or "utf-8", "replace") - except Exception: - continue - return "(no readable text body)" - payload = msg.get_payload(decode=True) - if payload is None: - return str(msg.get_payload()) - return payload.decode(msg.get_content_charset() or "utf-8", "replace") - - -def _match_workflow(con, recipients, subject): - workflows = con.execute("SELECT * FROM workflows WHERE active=1").fetchall() - by_slug = {w["slug"]: w for w in workflows} - - # 1. plus-addressing: inbox+slug@... - for addr in recipients: - m = re.match(r"^[^+@]+\+([a-z0-9\-]+)@", addr) - if m and m.group(1) in by_slug: - return by_slug[m.group(1)], "plus-address" - - # 2. subject tag [slug] - for slug, w in by_slug.items(): - if f"[{slug}]" in subject.lower(): - return w, "subject tag" - - # 3. single active workflow fallback - if len(workflows) == 1: - return workflows[0], "only active workflow" - return None, None - - -def process_message(raw_bytes): - msg = email.message_from_bytes(raw_bytes) - sender = (email.utils.parseaddr(msg.get("From", ""))[1] or "").lower() - recipients = _addr_list(msg, "To") + _addr_list(msg, "Cc") - subject = msg.get("Subject", "(no subject)") - body = _body_text(msg).strip() - - con = db.get_db() - try: - wf, how = _match_workflow(con, recipients, subject) - if wf is None: - print(f"[poller] No workflow matched mail from {sender} — ignored.") - return - - # CC'd people are "involved up to this point" too — everyone on To/Cc - # except the sender and the intake mailbox itself (incl. its +tag variants) - inbox_local = (os.environ.get("IMAP_USER") or "").split("@")[0].lower() - - def is_intake(addr): - base = addr.split("@")[0].split("+")[0] - return (inbox_local and base == inbox_local) or \ - addr.split("@")[0].endswith("+" + wf["slug"]) - - ccs = [a for a in recipients if a != sender and not is_intake(a)] - rid, token = db.create_request(con, wf["id"], "email", sender, subject, body, extra_participants=ccs) - db.log(con, rid, "matched", f"Matched workflow '{wf['name']}' via {how}") - con.commit() - - pipeline.send_approval_request(rid) - print(f"[poller] Request #{rid} created for workflow '{wf['name']}' from {sender}.") - except Exception: - traceback.print_exc() - finally: - con.close() - - -def poll_loop(): - host = os.environ.get("IMAP_HOST", "") - user = os.environ.get("IMAP_USER", "") - password = os.environ.get("IMAP_PASSWORD", "") - folder = os.environ.get("IMAP_FOLDER", "INBOX") - interval = int(os.environ.get("IMAP_POLL_SECONDS", "30")) - - if not host: - print("[poller] IMAP_HOST not set — email intake disabled (web form still works).") - return - - print(f"[poller] Watching {user} on {host} every {interval}s.") - while True: - try: - imap = imaplib.IMAP4_SSL(host) - imap.login(user, password) - imap.select(folder) - _, data = imap.search(None, "UNSEEN") - for num in data[0].split(): - _, msg_data = imap.fetch(num, "(RFC822)") - process_message(msg_data[0][1]) - imap.store(num, "+FLAGS", "\\Seen") - imap.logout() - except Exception as e: - print(f"[poller] Error: {e}") - time.sleep(interval) - - -def start(): - t = threading.Thread(target=poll_loop, daemon=True) - t.start() diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/static/style.css deleted file mode 100644 index f71bca8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied, .stamp.rejected { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index c617951..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
{{ stats.get('rejected', 0) }}Rejected
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - {% else %} - - {% endfor %} -
NameIntake tagApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
+{{ w.slug }} / [{{ w.slug }}]{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectSourceStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.source }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 7fc20f3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 92cc510..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 9978c99..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 46c2076..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · via {{ req.source }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'pending' %} -
- - - Same effect as the current approver's email links. -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 13df989..0000000 --- a/Approval Workflow/OLD_VERSIONS/v2.0/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - - {% if wf %}

Email intake: send to inbox+{{ wf.slug }}@your-domain or put [{{ wf.slug }}] in the subject.

{% endif %} -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow.zip deleted file mode 100644 index 55fe614..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/README.md deleted file mode 100644 index 3809c98..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending + template rendering -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/app.py deleted file mode 100644 index 454b71c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,296 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not subject or not body: - flash("Subject and message are both required.", "error") - else: - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - requester = user["email"] or user["username"] - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.source='web' AND (r.requester=? OR r.requester=?) ORDER BY r.id DESC LIMIT 15", - (session["username"], - (g.db.execute("SELECT email FROM users WHERE id=?", (session["user_id"],)).fetchone()["email"] or "-"), - )).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests) - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - status, req2, message = pipeline.decide(token, action, decided_by) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)") - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/db.py deleted file mode 100644 index b022965..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,176 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 33d3e6b..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Outbound email via Purelymail SMTP, and template rendering. - -The server is preconfigured for Purelymail (smtp.purelymail.com:465, SSL/TLS). -Only the account's email address and password need to be provided: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) -""" -import os -import smtplib -from email.message import EmailMessage - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def send_mail(to_addrs, subject, body): - """Send a plain-text email through Purelymail. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - return True, f"sent to {', '.join(to_addrs)}" - except Exception as e: - return False, f"SMTP error: {e}" diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index cf134a3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - v = _vars(wf, req) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(wf["approval_body"], **v)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper()) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by): - """Handle a decision at the request's current step. - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/static/style.css deleted file mode 100644 index f71bca8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied, .stamp.rejected { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 6055cf7..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
{{ stats.get('rejected', 0) }}Rejected
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 7fc20f3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 92cc510..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 9978c99..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 714a2c8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'pending' %} -
- - - Same effect as the current approver's email links. -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 869b6e5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v3.0/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow.zip deleted file mode 100644 index d0a46cd..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/README.md deleted file mode 100644 index 79e7802..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/app.py deleted file mode 100644 index 9ec9b86..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,311 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not subject or not body: - flash("Subject and message are both required.", "error") - else: - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - requester = user["email"] or user["username"] - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.source='web' AND (r.requester=? OR r.requester=?) ORDER BY r.id DESC LIMIT 15", - (session["username"], - (g.db.execute("SELECT email FROM users WHERE id=?", (session["user_id"],)).fetchone()["email"] or "-"), - )).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests) - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - status, req2, message = pipeline.decide(token, action, decided_by) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)") - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/db.py deleted file mode 100644 index 311a381..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,189 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 9a22444..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None): - """Send a plain-text email through Purelymail, keep a copy in the Sent - folder, and record the attempt in the mail log. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 0645181..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(wf["approval_body"], **v), - request_id=request_id) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper()) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v), - request_id=req["id"]) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by): - """Handle a decision at the request's current step. - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/static/style.css deleted file mode 100644 index f71bca8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied, .stamp.rejected { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 6055cf7..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
{{ stats.get('rejected', 0) }}Rejected
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index e1ceecc..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 92cc510..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 9978c99..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index a1f2bda..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — ApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 714a2c8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'pending' %} -
- - - Same effect as the current approver's email links. -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 869b6e5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v4.0 w/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow.zip deleted file mode 100644 index a9414d4..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/README.md deleted file mode 100644 index 492c09c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/app.py deleted file mode 100644 index 1c99b7f..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,322 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/db.py deleted file mode 100644 index 8b7ea9d..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,204 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 9a22444..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None): - """Send a plain-text email through Purelymail, keep a copy in the Sent - folder, and record the attempt in the mail log. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 4264b1e..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(wf["approval_body"], **v), - request_id=request_id) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v), - request_id=req["id"]) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/static/style.css deleted file mode 100644 index f71bca8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied, .stamp.rejected { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 6055cf7..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
{{ stats.get('rejected', 0) }}Rejected
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index e1ceecc..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 9f5391a..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 35c4a26..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index a1f2bda..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — ApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index bf022c8..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 63776d5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v5.0 w/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow.zip deleted file mode 100644 index e312724..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/README.md deleted file mode 100644 index 492c09c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/app.py deleted file mode 100644 index fb633a5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,377 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash - -import db -import pipeline -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/db.py deleted file mode 100644 index b182235..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,207 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 9a22444..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None): - """Send a plain-text email through Purelymail, keep a copy in the Sent - folder, and record the attempt in the mail log. Returns (ok, detail).""" - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index e63850d..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(body_t, **v), - request_id=request_id) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v), - request_id=req["id"]) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/static/style.css deleted file mode 100644 index ae26b67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,136 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 9163bca..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — ApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index e1ceecc..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -{% block title %}ApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- ApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 9f5391a..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 35c4a26..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — ApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index f662b52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — ApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index a1f2bda..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — ApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 91d4afe..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — ApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 2df58dd..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — ApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 06b1a3c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v6.0 w/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — ApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow.zip deleted file mode 100644 index 4be3a95..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/README.md deleted file mode 100644 index d7dd830..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one uploaded file (up to 15 MB). It is attached to **every approval mail** sent along the chain, and admins can download it from the request page. Files are stored next to the database in `data/attachments/`. -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/app.py deleted file mode 100644 index 223876f..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,411 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded file is too large — the limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(request.args.get("next") or url_for("index")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - upload = request.files.get("attachment") - file_data = upload.read() if upload and upload.filename else b"" - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif len(file_data) > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded file is too large — the limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if file_data: - stored = f"{rid}_{secure_filename(upload.filename) or 'attachment'}" - os.makedirs(db.ATTACH_DIR, exist_ok=True) - with open(db.attachment_path(stored), "wb") as fh: - fh.write(file_data) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", (stored, rid)) - db.log(g.db, rid, "attachment saved", - f"'{upload.filename}' ({len(file_data)} bytes) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -@app.route("/admin/request//attachment") -@admin_required -def request_attachment(rid): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None or not req["attachment"]: - abort(404) - path = db.attachment_path(req["attachment"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, - download_name=req["attachment"].split("_", 1)[-1]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/db.py deleted file mode 100644 index eb4f085..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,218 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '', -- stored filename of the uploaded file, if any - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 24033e3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - msg.set_content(body) - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index f516f33..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,216 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def load_attachment(req): - """Load the request's uploaded file (if any) as a send_mail attachment list.""" - if not req["attachment"]: - return None - path = db.attachment_path(req["attachment"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - return None - # the stored name is "_"; show the original name in the mail - original = req["attachment"].split("_", 1)[-1] - ctype = mimetypes.guess_type(original)[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - return [(original, data, maintype, subtype)] - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachment(req) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - render(body_t, **v), - request_id=request_id, - attachments=attachments) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - ok, detail = send_mail(participants, render(subj_t, **v), render(body_t, **v), - request_id=req["id"]) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 16fec52..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,143 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root { - --paper: #fafbfc; - --card: #ffffff; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --radius: 8px; - font-size: 16px; -} -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: #fff; -} -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: #f2f6f8; } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: #e4f3ee; border: 1px solid #bfe0d5; color: #0b5c4c; } -.flash.error { background: #f9e8e6; border: 1px solid #edc7c3; color: #872a24; } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: #f4f6f8; border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 98913ab..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index eb8b41a..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- {{ session['username'] }} - Sign out -
-
-{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz
- - diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 7cb6b61..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 1706e20..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index a6cb09d..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 1a8e5c2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.attachment %} -

Attachment: - {{ req.attachment.split('_', 1)[-1] }} - (included in every approval mail)

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 921350c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 4e12264..0000000 --- a/Approval Workflow/OLD_VERSIONS/v7.0 w/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow.zip deleted file mode 100644 index 0dedd1f..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/.env.example deleted file mode 100644 index 6e37c49..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/README.md deleted file mode 100644 index 088672f..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one uploaded file (up to 15 MB). It is attached to **every approval mail** sent along the chain, and admins can download it from the request page. Files are stored next to the database in `data/attachments/`. -- **MFA.** Any user can enable two-factor authentication from their Account page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/app.py deleted file mode 100644 index b24ab4e..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,539 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded file is too large — the limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - upload = request.files.get("attachment") - file_data = upload.read() if upload and upload.filename else b"" - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif len(file_data) > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded file is too large — the limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if file_data: - stored = f"{rid}_{secure_filename(upload.filename) or 'attachment'}" - os.makedirs(db.ATTACH_DIR, exist_ok=True) - with open(db.attachment_path(stored), "wb") as fh: - fh.write(file_data) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", (stored, rid)) - db.log(g.db, rid, "attachment saved", - f"'{upload.filename}' ({len(file_data)} bytes) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -@app.route("/admin/request//attachment") -@admin_required -def request_attachment(rid): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None or not req["attachment"]: - abort(404) - path = db.attachment_path(req["attachment"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, - download_name=req["attachment"].split("_", 1)[-1]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/db.py deleted file mode 100644 index 7d5ca9e..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,227 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '', -- stored filename of the uploaded file, if any - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 732a911..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def load_attachment(req): - """Load the request's uploaded file (if any) as a send_mail attachment list.""" - if not req["attachment"]: - return None - path = db.attachment_path(req["attachment"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - return None - # the stored name is "_"; show the original name in the mail - original = req["attachment"].split("_", 1)[-1] - ctype = mimetypes.guess_type(original)[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - return [(original, data, maintype, subtype)] - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachment(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/account.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/account.html deleted file mode 100644 index aa1fbfe..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/account.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} -{% block title %}Account — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Account

-

Signed in as {{ user.username }}{% if user.email %} · {{ user.email }}{% endif %}

- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 98913ab..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index ed990e5..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Account - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz
- - diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 7cb6b61..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 1706e20..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 1a8e5c2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.attachment %} -

Attachment: - {{ req.attachment.split('_', 1)[-1] }} - (included in every approval mail)

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 223e422..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users enable MFA themselves on their Account page; you can disable it here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 7678fc1..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/totp.py b/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/OLD_VERSIONS/v8.0 w/approvalflow/approvalflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow.zip deleted file mode 100644 index b69f098..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/.env.example deleted file mode 100644 index 51b61a3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: Docker Compose treats '$' in this file as variable substitution. -# If any value (e.g. a password) contains a literal '$', write it as '$$': -# MAIL_PASSWORD=xK9$$vh4pQ2 -> the container receives xK9$vh4pQ2 -# Otherwise you'll see a warning like: The "..." variable is not set. - -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/README.md deleted file mode 100644 index 08601c1..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one uploaded file (up to 15 MB). It is attached to **every approval mail** sent along the chain, and admins can download it from the request page. Files are stored next to the database in `data/attachments/`. -- **Configure profile.** Every user has a profile page (top-right corner): first name, last name, unit location, e-mail, change password, and MFA. The profile e-mail pre-fills the mandatory "Request's e-mail" field when submitting a request. -- **MFA.** Any user can enable two-factor authentication from their Configure profile page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -> **If a value in `.env` contains a `$`** (common in passwords), escape it as `$$` — e.g. `MAIL_PASSWORD=xK9$$vh4pQ2`. Docker Compose otherwise treats `$word` as a variable and silently replaces it with a blank string, printing a warning like `The "word" variable is not set`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/app.py deleted file mode 100644 index 3021fe4..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,566 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded file is too large — the limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "profile": - email = request.form.get("email", "").strip().lower() - if email and "@" not in email: - flash("That e-mail address doesn't look valid.", "error") - else: - g.db.execute( - "UPDATE users SET first_name=?, last_name=?, unit_location=?, email=? WHERE id=?", - (request.form.get("first_name", "").strip(), - request.form.get("last_name", "").strip(), - request.form.get("unit_location", "").strip(), - email, user["id"])) - g.db.commit() - flash("Profile saved.", "ok") - return redirect(url_for("account")) - if act == "password": - if not check_password_hash(user["password_hash"], request.form.get("current_password", "")): - flash("Your current password is wrong.", "error") - elif not request.form.get("new_password"): - flash("The new password can't be empty.", "error") - elif request.form.get("new_password") != request.form.get("confirm_password"): - flash("The new passwords don't match.", "error") - else: - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(request.form["new_password"]), user["id"])) - g.db.commit() - flash("Password changed.", "ok") - return redirect(url_for("account")) - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - upload = request.files.get("attachment") - file_data = upload.read() if upload and upload.filename else b"" - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif len(file_data) > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded file is too large — the limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if file_data: - stored = f"{rid}_{secure_filename(upload.filename) or 'attachment'}" - os.makedirs(db.ATTACH_DIR, exist_ok=True) - with open(db.attachment_path(stored), "wb") as fh: - fh.write(file_data) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", (stored, rid)) - db.log(g.db, rid, "attachment saved", - f"'{upload.filename}' ({len(file_data)} bytes) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain) - - -@app.route("/admin/request//attachment") -@admin_required -def request_attachment(rid): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None or not req["attachment"]: - abort(404) - path = db.attachment_path(req["attachment"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, - download_name=req["attachment"].split("_", 1)[-1]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/db.py deleted file mode 100644 index f2dd27c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,234 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - first_name TEXT NOT NULL DEFAULT '', - last_name TEXT NOT NULL DEFAULT '', - unit_location TEXT NOT NULL DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '', -- stored filename of the uploaded file, if any - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - for col in ("first_name", "last_name", "unit_location"): - if ucols and col not in ucols: - con.execute(f"ALTER TABLE users ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") - print(f"[migrate] Added {col} to users.") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 732a911..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def load_attachment(req): - """Load the request's uploaded file (if any) as a send_mail attachment list.""" - if not req["attachment"]: - return None - path = db.attachment_path(req["attachment"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - return None - # the stored name is "_"; show the original name in the mail - original = req["attachment"].split("_", 1)[-1] - ctype = mimetypes.guess_type(original)[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - return [(original, data, maintype, subtype)] - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachment(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/account.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/account.html deleted file mode 100644 index 90eb590..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/account.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% block title %}Configure profile — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Configure profile

-

Signed in as {{ user.username }}

- -
-

Profile

-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Change password

-
- -
-
- - -
-
-
- - -
-
- - -
-
- -
-
- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index 98913ab..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 5491762..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Configure profile - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz
- - diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 7cb6b61..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 1706e20..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index 1a8e5c2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if req.attachment %} -

Attachment: - {{ req.attachment.split('_', 1)[-1] }} - (included in every approval mail)

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 3f658f9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users fill in their own details (name, unit, e-mail, MFA) on their Configure profile page; you can disable MFA here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.first_name or u.last_name %}{{ u.first_name }} {{ u.last_name }}{% endif %} - {% if u.unit_location %}{{ u.unit_location }}{% endif %} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 7678fc1..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
- - - -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/totp.py b/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.0 final/approvalflow/approvalflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow.zip deleted file mode 100644 index 8cb7e2e..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/.env.example deleted file mode 100644 index 51b61a3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: Docker Compose treats '$' in this file as variable substitution. -# If any value (e.g. a password) contains a literal '$', write it as '$$': -# MAIL_PASSWORD=xK9$$vh4pQ2 -> the container receives xK9$vh4pQ2 -# Otherwise you'll see a warning like: The "..." variable is not set. - -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/README.md deleted file mode 100644 index d908d5c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one or more uploaded files (15 MB combined). They are attached to **every approval mail** sent along the chain, and admins can download each from the request page. Files are stored next to the database in `data/attachments/`. -- **Knowledge chain.** Next to each workflow's approval chain there is a knowledge chain: addresses that never approve anything but are informed by e-mail when a request is created and on every step approval, denial, or final approval. They are not counted as "involved" and don't receive the participant outcome notice — only their own FYI messages. -- **Configure profile.** Every user has a profile page (top-right corner): first name, last name, unit location, e-mail, change password, and MFA. The profile e-mail pre-fills the mandatory "Request's e-mail" field when submitting a request. -- **MFA.** Any user can enable two-factor authentication from their Configure profile page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -> **If a value in `.env` contains a `$`** (common in passwords), escape it as `$$` — e.g. `MAIL_PASSWORD=xK9$$vh4pQ2`. Docker Compose otherwise treats `$word` as a variable and silently replaces it with a blank string, printing a warning like `The "word" variable is not set`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Upgrading an existing instance (no data loss) - -All schema changes are applied automatically at boot by the built-in migrations — -the SQLite database and uploaded files live in `./data/`, which is a volume and is -never touched by rebuilding the image. - -```bash -cd /path/to/approvalflow -docker compose down # stop the app (data stays in ./data) -cp -r data data.backup-$(date +%F) # safety backup of DB + attachments -# replace the code with the new version, KEEPING .env and data/: -# unzip -o approvalflow.zip -d .. (or copy the new files over the old ones) -docker compose up -d --build # rebuild image, start; migrations run at boot -docker compose logs | grep migrate # optional: see what was migrated -``` - -Roll back = stop, restore `data.backup-…` to `data/`, start the previous image. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/app.py deleted file mode 100644 index 22bd546..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,579 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "profile": - email = request.form.get("email", "").strip().lower() - if email and "@" not in email: - flash("That e-mail address doesn't look valid.", "error") - else: - g.db.execute( - "UPDATE users SET first_name=?, last_name=?, unit_location=?, email=? WHERE id=?", - (request.form.get("first_name", "").strip(), - request.form.get("last_name", "").strip(), - request.form.get("unit_location", "").strip(), - email, user["id"])) - g.db.commit() - flash("Profile saved.", "ok") - return redirect(url_for("account")) - if act == "password": - if not check_password_hash(user["password_hash"], request.form.get("current_password", "")): - flash("Your current password is wrong.", "error") - elif not request.form.get("new_password"): - flash("The new password can't be empty.", "error") - elif request.form.get("new_password") != request.form.get("confirm_password"): - flash("The new passwords don't match.", "error") - else: - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(request.form["new_password"]), user["id"])) - g.db.commit() - flash("Password changed.", "ok") - return redirect(url_for("account")) - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - uploads = [(f, f.read()) for f in request.files.getlist("attachments") - if f and f.filename] - total = sum(len(data) for _, data in uploads) - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif total > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if uploads: - os.makedirs(db.ATTACH_DIR, exist_ok=True) - entries = [] - for i, (f, data) in enumerate(uploads, 1): - name = secure_filename(f.filename) or f"attachment-{i}" - stored = f"{rid}_{i}_{name}" - with open(db.attachment_path(stored), "wb") as fh: - fh.write(data) - entries.append({"stored": stored, "name": name}) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entries), rid)) - db.log(g.db, rid, "attachments saved", - ", ".join(f"'{e['name']}'" for e in entries) + - f" ({total} bytes total) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - pipeline.notify_knowledge(rid, f"New request created by {requester}") - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain, - attachments=pipeline.attachment_list(req)) - - -@app.route("/admin/request//attachment/") -@admin_required -def request_attachment(rid, idx): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None: - abort(404) - entries = pipeline.attachment_list(req) - if idx < 0 or idx >= len(entries): - abort(404) - path = db.attachment_path(entries[idx]["stored"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, download_name=entries[idx]["name"]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers, w.knowledge FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"Knowledge chain: {', '.join(pipeline.knowledge_chain(req)) or '(none)'}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - knowledge = [a.strip() for a in f.get("knowledge", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, knowledge, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?, knowledge=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/db.py deleted file mode 100644 index 145b578..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,249 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - first_name TEXT NOT NULL DEFAULT '', - last_name TEXT NOT NULL DEFAULT '', - unit_location TEXT NOT NULL DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - knowledge TEXT NOT NULL DEFAULT '[]', -- JSON list: informed of every event, never approve - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '[]', -- JSON list of {"stored": ..., "name": ...} - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - for col in ("first_name", "last_name", "unit_location"): - if ucols and col not in ucols: - con.execute(f"ALTER TABLE users ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") - print(f"[migrate] Added {col} to users.") - wcols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if wcols and "knowledge" not in wcols: - con.execute("ALTER TABLE workflows ADD COLUMN knowledge TEXT NOT NULL DEFAULT '[]'") - print("[migrate] Added knowledge chain to workflows.") - # single-attachment values -> JSON list format - if rcols and "attachment" in rcols: - for row in con.execute("SELECT id, attachment FROM requests " - "WHERE attachment != '' AND attachment NOT LIKE '[%'").fetchall(): - stored = row["attachment"] - entry = [{"stored": stored, "name": stored.split("_", 1)[-1]}] - con.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entry), row["id"])) - print(f"[migrate] Converted attachment of request #{row['id']} to list format.") - con.execute("UPDATE requests SET attachment='[]' WHERE attachment=''") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 9a77bab..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def attachment_list(req): - """Parse the request's attachment column into [{"stored":…, "name":…}, …].""" - try: - return json.loads(req["attachment"] or "[]") - except Exception: - return [] - - -def load_attachments(req): - """Load every uploaded file of the request as send_mail attachments.""" - out = [] - for entry in attachment_list(req): - path = db.attachment_path(entry["stored"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - continue - ctype = mimetypes.guess_type(entry["name"])[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - out.append((entry["name"], data, maintype, subtype)) - return out or None - - -def knowledge_chain(wf): - try: - chain = json.loads(wf["knowledge"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def notify_knowledge(request_id, event): - """Inform the workflow's knowledge chain of an event. These addresses are - never part of the approval chain — they only receive FYI messages.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - watchers = knowledge_chain(wf) - if not watchers: - return - chain = approver_chain(wf) - subject = f"[{wf['name']}] FYI — request #{req['id']}: {event}" - body = ( - f"For your information (you are on the knowledge chain of this workflow —\n" - f"no action is needed from you):\n\n" - f"{event}\n\n" - f"Workflow: {wf['name']}\n" - f"From: {req['requester']}\n" - f"Created: {req['created_at']}\n" - f"Subject: {req['subject']}\n" - f"Status: {req['status'].upper()}" - f" (step {min(req['current_step'], max(len(chain)-1, 0)) + 1} of {len(chain)})\n\n" - f"Message:\n{req['body']}\n" - ) - if req["deny_reason"]: - body += f"\nReason for denial:\n{req['deny_reason']}\n" - ok, detail = send_mail(watchers, subject, body, request_id=request_id) - db.log(con, request_id, - "knowledge chain informed" if ok else "mail error", - f"'{event}' to {', '.join(watchers)} — {detail}") - con.commit() - finally: - con.close() - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachments(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - notify_knowledge(req["id"], f"DENIED at step {step + 1}/{len(chain)} by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - notify_knowledge(req["id"], - f"FULLY APPROVED — final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - notify_knowledge(req["id"], f"Step {step + 1}/{len(chain)} approved by {decided_by} — " - f"now waiting on {chain[step + 1]}") - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/account.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/account.html deleted file mode 100644 index 90eb590..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/account.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% block title %}Configure profile — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Configure profile

-

Signed in as {{ user.username }}

- -
-

Profile

-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Change password

-
- -
-
- - -
-
-
- - -
-
- - -
-
- -
-
- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index d42a305..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainKnowledge chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% for a in w.knowledge | fromjson %}{{ a }}{% if not loop.last %}
{% endif %}{% else %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 5491762..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Configure profile - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz
- - diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 4e54315..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 00215d2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index b6e8c1c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if attachments %} -

Attachments (included in every approval mail):
- {% for att in attachments %} - {{ att.name }}{% if not loop.last %} · {% endif %} - {% endfor %} -

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 3f658f9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users fill in their own details (name, unit, e-mail, MFA) on their Configure profile page; you can disable MFA here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.first_name or u.last_name %}{{ u.first_name }} {{ u.last_name }}{% endif %} - {% if u.unit_location %}{{ u.unit_location }}{% endif %} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 47202ae..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/totp.py b/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.1 patch/approvalflow/approvalflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow.zip deleted file mode 100644 index 9d22469..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/.env.example deleted file mode 100644 index 51b61a3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: Docker Compose treats '$' in this file as variable substitution. -# If any value (e.g. a password) contains a literal '$', write it as '$$': -# MAIL_PASSWORD=xK9$$vh4pQ2 -> the container receives xK9$vh4pQ2 -# Otherwise you'll see a warning like: The "..." variable is not set. - -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/README.md deleted file mode 100644 index d908d5c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one or more uploaded files (15 MB combined). They are attached to **every approval mail** sent along the chain, and admins can download each from the request page. Files are stored next to the database in `data/attachments/`. -- **Knowledge chain.** Next to each workflow's approval chain there is a knowledge chain: addresses that never approve anything but are informed by e-mail when a request is created and on every step approval, denial, or final approval. They are not counted as "involved" and don't receive the participant outcome notice — only their own FYI messages. -- **Configure profile.** Every user has a profile page (top-right corner): first name, last name, unit location, e-mail, change password, and MFA. The profile e-mail pre-fills the mandatory "Request's e-mail" field when submitting a request. -- **MFA.** Any user can enable two-factor authentication from their Configure profile page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -> **If a value in `.env` contains a `$`** (common in passwords), escape it as `$$` — e.g. `MAIL_PASSWORD=xK9$$vh4pQ2`. Docker Compose otherwise treats `$word` as a variable and silently replaces it with a blank string, printing a warning like `The "word" variable is not set`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Upgrading an existing instance (no data loss) - -All schema changes are applied automatically at boot by the built-in migrations — -the SQLite database and uploaded files live in `./data/`, which is a volume and is -never touched by rebuilding the image. - -```bash -cd /path/to/approvalflow -docker compose down # stop the app (data stays in ./data) -cp -r data data.backup-$(date +%F) # safety backup of DB + attachments -# replace the code with the new version, KEEPING .env and data/: -# unzip -o approvalflow.zip -d .. (or copy the new files over the old ones) -docker compose up -d --build # rebuild image, start; migrations run at boot -docker compose logs | grep migrate # optional: see what was migrated -``` - -Roll back = stop, restore `data.backup-…` to `data/`, start the previous image. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/app.py deleted file mode 100644 index 22bd546..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,579 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "profile": - email = request.form.get("email", "").strip().lower() - if email and "@" not in email: - flash("That e-mail address doesn't look valid.", "error") - else: - g.db.execute( - "UPDATE users SET first_name=?, last_name=?, unit_location=?, email=? WHERE id=?", - (request.form.get("first_name", "").strip(), - request.form.get("last_name", "").strip(), - request.form.get("unit_location", "").strip(), - email, user["id"])) - g.db.commit() - flash("Profile saved.", "ok") - return redirect(url_for("account")) - if act == "password": - if not check_password_hash(user["password_hash"], request.form.get("current_password", "")): - flash("Your current password is wrong.", "error") - elif not request.form.get("new_password"): - flash("The new password can't be empty.", "error") - elif request.form.get("new_password") != request.form.get("confirm_password"): - flash("The new passwords don't match.", "error") - else: - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(request.form["new_password"]), user["id"])) - g.db.commit() - flash("Password changed.", "ok") - return redirect(url_for("account")) - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - uploads = [(f, f.read()) for f in request.files.getlist("attachments") - if f and f.filename] - total = sum(len(data) for _, data in uploads) - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif total > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if uploads: - os.makedirs(db.ATTACH_DIR, exist_ok=True) - entries = [] - for i, (f, data) in enumerate(uploads, 1): - name = secure_filename(f.filename) or f"attachment-{i}" - stored = f"{rid}_{i}_{name}" - with open(db.attachment_path(stored), "wb") as fh: - fh.write(data) - entries.append({"stored": stored, "name": name}) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entries), rid)) - db.log(g.db, rid, "attachments saved", - ", ".join(f"'{e['name']}'" for e in entries) + - f" ({total} bytes total) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - pipeline.notify_knowledge(rid, f"New request created by {requester}") - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain, - attachments=pipeline.attachment_list(req)) - - -@app.route("/admin/request//attachment/") -@admin_required -def request_attachment(rid, idx): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None: - abort(404) - entries = pipeline.attachment_list(req) - if idx < 0 or idx >= len(entries): - abort(404) - path = db.attachment_path(entries[idx]["stored"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, download_name=entries[idx]["name"]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers, w.knowledge FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"Knowledge chain: {', '.join(pipeline.knowledge_chain(req)) or '(none)'}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - knowledge = [a.strip() for a in f.get("knowledge", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, knowledge, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?, knowledge=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/db.py deleted file mode 100644 index 145b578..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,249 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - first_name TEXT NOT NULL DEFAULT '', - last_name TEXT NOT NULL DEFAULT '', - unit_location TEXT NOT NULL DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - knowledge TEXT NOT NULL DEFAULT '[]', -- JSON list: informed of every event, never approve - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '[]', -- JSON list of {"stored": ..., "name": ...} - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - for col in ("first_name", "last_name", "unit_location"): - if ucols and col not in ucols: - con.execute(f"ALTER TABLE users ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") - print(f"[migrate] Added {col} to users.") - wcols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if wcols and "knowledge" not in wcols: - con.execute("ALTER TABLE workflows ADD COLUMN knowledge TEXT NOT NULL DEFAULT '[]'") - print("[migrate] Added knowledge chain to workflows.") - # single-attachment values -> JSON list format - if rcols and "attachment" in rcols: - for row in con.execute("SELECT id, attachment FROM requests " - "WHERE attachment != '' AND attachment NOT LIKE '[%'").fetchall(): - stored = row["attachment"] - entry = [{"stored": stored, "name": stored.split("_", 1)[-1]}] - con.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entry), row["id"])) - print(f"[migrate] Converted attachment of request #{row['id']} to list format.") - con.execute("UPDATE requests SET attachment='[]' WHERE attachment=''") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 9a77bab..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def attachment_list(req): - """Parse the request's attachment column into [{"stored":…, "name":…}, …].""" - try: - return json.loads(req["attachment"] or "[]") - except Exception: - return [] - - -def load_attachments(req): - """Load every uploaded file of the request as send_mail attachments.""" - out = [] - for entry in attachment_list(req): - path = db.attachment_path(entry["stored"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - continue - ctype = mimetypes.guess_type(entry["name"])[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - out.append((entry["name"], data, maintype, subtype)) - return out or None - - -def knowledge_chain(wf): - try: - chain = json.loads(wf["knowledge"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def notify_knowledge(request_id, event): - """Inform the workflow's knowledge chain of an event. These addresses are - never part of the approval chain — they only receive FYI messages.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - watchers = knowledge_chain(wf) - if not watchers: - return - chain = approver_chain(wf) - subject = f"[{wf['name']}] FYI — request #{req['id']}: {event}" - body = ( - f"For your information (you are on the knowledge chain of this workflow —\n" - f"no action is needed from you):\n\n" - f"{event}\n\n" - f"Workflow: {wf['name']}\n" - f"From: {req['requester']}\n" - f"Created: {req['created_at']}\n" - f"Subject: {req['subject']}\n" - f"Status: {req['status'].upper()}" - f" (step {min(req['current_step'], max(len(chain)-1, 0)) + 1} of {len(chain)})\n\n" - f"Message:\n{req['body']}\n" - ) - if req["deny_reason"]: - body += f"\nReason for denial:\n{req['deny_reason']}\n" - ok, detail = send_mail(watchers, subject, body, request_id=request_id) - db.log(con, request_id, - "knowledge chain informed" if ok else "mail error", - f"'{event}' to {', '.join(watchers)} — {detail}") - con.commit() - finally: - con.close() - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachments(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - notify_knowledge(req["id"], f"DENIED at step {step + 1}/{len(chain)} by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - notify_knowledge(req["id"], - f"FULLY APPROVED — final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - notify_knowledge(req["id"], f"Step {step + 1}/{len(chain)} approved by {decided_by} — " - f"now waiting on {chain[step + 1]}") - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/account.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/account.html deleted file mode 100644 index 90eb590..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/account.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% block title %}Configure profile — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Configure profile

-

Signed in as {{ user.username }}

- -
-

Profile

-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Change password

-
- -
-
- - -
-
-
- - -
-
- - -
-
- -
-
- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index d42a305..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainKnowledge chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% for a in w.knowledge | fromjson %}{{ a }}{% if not loop.last %}
{% endif %}{% else %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 208d9af..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Configure profile - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz - Version 9.2
- - diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 4e54315..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 00215d2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index b6e8c1c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if attachments %} -

Attachments (included in every approval mail):
- {% for att in attachments %} - {{ att.name }}{% if not loop.last %} · {% endif %} - {% endfor %} -

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 3f658f9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users fill in their own details (name, unit, e-mail, MFA) on their Configure profile page; you can disable MFA here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.first_name or u.last_name %}{{ u.first_name }} {{ u.last_name }}{% endif %} - {% if u.unit_location %}{{ u.unit_location }}{% endif %} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 47202ae..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/totp.py b/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.2 patch/approvalflow/approvalflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow.zip b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow.zip deleted file mode 100644 index 711c1e3..0000000 Binary files a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow.zip and /dev/null differ diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/.env.example b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/.env.example deleted file mode 100644 index 51b61a3..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: Docker Compose treats '$' in this file as variable substitution. -# If any value (e.g. a password) contains a literal '$', write it as '$$': -# MAIL_PASSWORD=xK9$$vh4pQ2 -> the container receives xK9$vh4pQ2 -# Otherwise you'll see a warning like: The "..." variable is not set. - -# --- Web app --- -SECRET_KEY=change-me-to-a-long-random-string -BASE_URL=http://localhost:8000 # public URL used in approve/deny links -PORT=8000 -ADMIN_USERNAME=admin # created on first run only -ADMIN_PASSWORD=changeme - -# --- Purelymail account (only login and password are needed) --- -# Server settings are preconfigured: smtp.purelymail.com, port 465, SSL/TLS. -# If Two Factor Authentication is enabled on the account, use an App Password. -# Leave MAIL_USER empty to print outgoing mail to the console (dev mode). -MAIL_USER=you@yourdomain.com -MAIL_PASSWORD=secret diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/Dockerfile b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/Dockerfile deleted file mode 100644 index 0121b92..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATABASE_PATH=/data/approvalflow.db -VOLUME /data -EXPOSE 8000 -CMD ["gunicorn", "-b", "0.0.0.0:8000", "--workers", "1", "--threads", "8", "app:app"] diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/README.md b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/README.md deleted file mode 100644 index 4883066..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one or more uploaded files (15 MB combined). They are attached to **every approval mail** sent along the chain, and admins can download each from the request page. Files are stored next to the database in `data/attachments/`. -- **Knowledge chain.** Next to each workflow's approval chain there is a knowledge chain: addresses that never approve anything but are informed by e-mail when a request is created and on every step approval, denial, or final approval. They are not counted as "involved" and don't receive the participant outcome notice — only their own FYI messages, which include the request's attached files just like the approval mails. -- **Configure profile.** Every user has a profile page (top-right corner): first name, last name, unit location, e-mail, change password, and MFA. The profile e-mail pre-fills the mandatory "Request's e-mail" field when submitting a request. -- **MFA.** Any user can enable two-factor authentication from their Configure profile page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -> **If a value in `.env` contains a `$`** (common in passwords), escape it as `$$` — e.g. `MAIL_PASSWORD=xK9$$vh4pQ2`. Docker Compose otherwise treats `$word` as a variable and silently replaces it with a blank string, printing a warning like `The "word" variable is not set`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Upgrading an existing instance (no data loss) - -All schema changes are applied automatically at boot by the built-in migrations — -the SQLite database and uploaded files live in `./data/`, which is a volume and is -never touched by rebuilding the image. - -```bash -cd /path/to/approvalflow -docker compose down # stop the app (data stays in ./data) -cp -r data data.backup-$(date +%F) # safety backup of DB + attachments -# replace the code with the new version, KEEPING .env and data/: -# unzip -o approvalflow.zip -d .. (or copy the new files over the old ones) -docker compose up -d --build # rebuild image, start; migrations run at boot -docker compose logs | grep migrate # optional: see what was migrated -``` - -Roll back = stop, restore `data.backup-…` to `data/`, start the previous image. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/app.py b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/app.py deleted file mode 100644 index 22bd546..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/app.py +++ /dev/null @@ -1,579 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "profile": - email = request.form.get("email", "").strip().lower() - if email and "@" not in email: - flash("That e-mail address doesn't look valid.", "error") - else: - g.db.execute( - "UPDATE users SET first_name=?, last_name=?, unit_location=?, email=? WHERE id=?", - (request.form.get("first_name", "").strip(), - request.form.get("last_name", "").strip(), - request.form.get("unit_location", "").strip(), - email, user["id"])) - g.db.commit() - flash("Profile saved.", "ok") - return redirect(url_for("account")) - if act == "password": - if not check_password_hash(user["password_hash"], request.form.get("current_password", "")): - flash("Your current password is wrong.", "error") - elif not request.form.get("new_password"): - flash("The new password can't be empty.", "error") - elif request.form.get("new_password") != request.form.get("confirm_password"): - flash("The new passwords don't match.", "error") - else: - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(request.form["new_password"]), user["id"])) - g.db.commit() - flash("Password changed.", "ok") - return redirect(url_for("account")) - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - uploads = [(f, f.read()) for f in request.files.getlist("attachments") - if f and f.filename] - total = sum(len(data) for _, data in uploads) - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif total > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if uploads: - os.makedirs(db.ATTACH_DIR, exist_ok=True) - entries = [] - for i, (f, data) in enumerate(uploads, 1): - name = secure_filename(f.filename) or f"attachment-{i}" - stored = f"{rid}_{i}_{name}" - with open(db.attachment_path(stored), "wb") as fh: - fh.write(data) - entries.append({"stored": stored, "name": name}) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entries), rid)) - db.log(g.db, rid, "attachments saved", - ", ".join(f"'{e['name']}'" for e in entries) + - f" ({total} bytes total) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - pipeline.notify_knowledge(rid, f"New request created by {requester}") - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain, - attachments=pipeline.attachment_list(req)) - - -@app.route("/admin/request//attachment/") -@admin_required -def request_attachment(rid, idx): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None: - abort(404) - entries = pipeline.attachment_list(req) - if idx < 0 or idx >= len(entries): - abort(404) - path = db.attachment_path(entries[idx]["stored"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, download_name=entries[idx]["name"]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers, w.knowledge FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"Knowledge chain: {', '.join(pipeline.knowledge_chain(req)) or '(none)'}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - knowledge = [a.strip() for a in f.get("knowledge", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, knowledge, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?, knowledge=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/db.py b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/db.py deleted file mode 100644 index 145b578..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/db.py +++ /dev/null @@ -1,249 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - first_name TEXT NOT NULL DEFAULT '', - last_name TEXT NOT NULL DEFAULT '', - unit_location TEXT NOT NULL DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - knowledge TEXT NOT NULL DEFAULT '[]', -- JSON list: informed of every event, never approve - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '[]', -- JSON list of {"stored": ..., "name": ...} - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - for col in ("first_name", "last_name", "unit_location"): - if ucols and col not in ucols: - con.execute(f"ALTER TABLE users ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") - print(f"[migrate] Added {col} to users.") - wcols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if wcols and "knowledge" not in wcols: - con.execute("ALTER TABLE workflows ADD COLUMN knowledge TEXT NOT NULL DEFAULT '[]'") - print("[migrate] Added knowledge chain to workflows.") - # single-attachment values -> JSON list format - if rcols and "attachment" in rcols: - for row in con.execute("SELECT id, attachment FROM requests " - "WHERE attachment != '' AND attachment NOT LIKE '[%'").fetchall(): - stored = row["attachment"] - entry = [{"stored": stored, "name": stored.split("_", 1)[-1]}] - con.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entry), row["id"])) - print(f"[migrate] Converted attachment of request #{row['id']} to list format.") - con.execute("UPDATE requests SET attachment='[]' WHERE attachment=''") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/docker-compose.yml b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/mailer.py b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/pipeline.py b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/pipeline.py deleted file mode 100644 index 8db535a..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/pipeline.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def attachment_list(req): - """Parse the request's attachment column into [{"stored":…, "name":…}, …].""" - try: - return json.loads(req["attachment"] or "[]") - except Exception: - return [] - - -def load_attachments(req): - """Load every uploaded file of the request as send_mail attachments.""" - out = [] - for entry in attachment_list(req): - path = db.attachment_path(entry["stored"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - continue - ctype = mimetypes.guess_type(entry["name"])[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - out.append((entry["name"], data, maintype, subtype)) - return out or None - - -def knowledge_chain(wf): - try: - chain = json.loads(wf["knowledge"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def notify_knowledge(request_id, event): - """Inform the workflow's knowledge chain of an event. These addresses are - never part of the approval chain — they only receive FYI messages.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - watchers = knowledge_chain(wf) - if not watchers: - return - chain = approver_chain(wf) - subject = f"[{wf['name']}] FYI — request #{req['id']}: {event}" - body = ( - f"For your information (you are on the knowledge chain of this workflow —\n" - f"no action is needed from you):\n\n" - f"{event}\n\n" - f"Workflow: {wf['name']}\n" - f"From: {req['requester']}\n" - f"Created: {req['created_at']}\n" - f"Subject: {req['subject']}\n" - f"Status: {req['status'].upper()}" - f" (step {min(req['current_step'], max(len(chain)-1, 0)) + 1} of {len(chain)})\n\n" - f"Message:\n{req['body']}\n" - ) - if req["deny_reason"]: - body += f"\nReason for denial:\n{req['deny_reason']}\n" - ok, detail = send_mail(watchers, subject, body, request_id=request_id, - attachments=load_attachments(req)) - db.log(con, request_id, - "knowledge chain informed" if ok else "mail error", - f"'{event}' to {', '.join(watchers)} — {detail}") - con.commit() - finally: - con.close() - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachments(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - notify_knowledge(req["id"], f"DENIED at step {step + 1}/{len(chain)} by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - notify_knowledge(req["id"], - f"FULLY APPROVED — final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - notify_knowledge(req["id"], f"Step {step + 1}/{len(chain)} approved by {decided_by} — " - f"now waiting on {chain[step + 1]}") - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/requirements.txt b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/static/style.css b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/account.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/account.html deleted file mode 100644 index 90eb590..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/account.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% block title %}Configure profile — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Configure profile

-

Signed in as {{ user.username }}

- -
-

Profile

-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Change password

-
- -
-
- - -
-
-
- - -
-
- - -
-
- -
-
- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/admin.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/admin.html deleted file mode 100644 index d42a305..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainKnowledge chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% for a in w.knowledge | fromjson %}{{ a }}{% if not loop.last %}
{% endif %}{% else %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/base.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/base.html deleted file mode 100644 index 26533d9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Configure profile - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz - Version 9.3
- - diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/decision.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/decision.html deleted file mode 100644 index 4e54315..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/decision.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/index.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/index.html deleted file mode 100644 index 00215d2..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/login.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/mail_status.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/request_detail.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/request_detail.html deleted file mode 100644 index b6e8c1c..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/request_detail.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if attachments %} -

Attachments (included in every approval mail):
- {% for att in attachments %} - {{ att.name }}{% if not loop.last %} · {% endif %} - {% endfor %} -

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/users.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/users.html deleted file mode 100644 index 3f658f9..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/users.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users fill in their own details (name, unit, e-mail, MFA) on their Configure profile page; you can disable MFA here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.first_name or u.last_name %}{{ u.first_name }} {{ u.last_name }}{% endif %} - {% if u.unit_location %}{{ u.unit_location }}{% endif %} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/workflow_edit.html b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/workflow_edit.html deleted file mode 100644 index 47202ae..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/templates/workflow_edit.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/totp.py b/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/OLD_VERSIONS/v9.3 patch/approvalflow/approvalflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Approval Workflow/README.md b/Approval Workflow/README.md deleted file mode 100644 index 4883066..0000000 --- a/Approval Workflow/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# ApprovalFlow - -A self-hosted approval-workflow app. Requests are created on the site (login required), move through each workflow's ordered chain of approvers by email, and once approved or denied, **everyone involved up to that point** is notified. Every request keeps a full audit log of everything that happened to it. - -Outgoing mail is preconfigured for **Purelymail** — you only provide the account's login and password. - -## What it does - -- **Sequential multi-step approval.** Every workflow has an ordered chain of approvers. The request goes to the first person; each approval sends it on to the next; the last approval completes it. **A denial at any step stops the workflow immediately** — nobody later in the chain is contacted. -- **Decisions notify everyone.** On the final outcome (approved or denied), the requester and every approver involved **up to that point** receive the outcome email. -- **Full log.** Every request records: received, each approval request sent, each step's decision, notifications, and any mail errors — each with a timestamp. -- **Attachments.** A request can include one or more uploaded files (15 MB combined). They are attached to **every approval mail** sent along the chain, and admins can download each from the request page. Files are stored next to the database in `data/attachments/`. -- **Knowledge chain.** Next to each workflow's approval chain there is a knowledge chain: addresses that never approve anything but are informed by e-mail when a request is created and on every step approval, denial, or final approval. They are not counted as "involved" and don't receive the participant outcome notice — only their own FYI messages, which include the request's attached files just like the approval mails. -- **Configure profile.** Every user has a profile page (top-right corner): first name, last name, unit location, e-mail, change password, and MFA. The profile e-mail pre-fills the mandatory "Request's e-mail" field when submitting a request. -- **MFA.** Any user can enable two-factor authentication from their Configure profile page (TOTP — Google Authenticator, Authy, 1Password, …). Sign-in then requires password + 6-digit code. Admins can disable MFA for a user who lost their device. -- **Light and dark mode.** A toggle in the top bar (and on the login page) switches themes; the choice is remembered per browser and defaults to the system preference. -- **Images in emails.** Admins upload PNG/JPG/GIF images (up to 2 MB each) in the library shown below the Email wording section of any workflow. Placing an image's `{image:name}` tag in the approval request body, approved notice body, or denied notice body embeds the image at that spot (HTML email with a plain-text fallback). -- **Mail status page (admin only).** Every outgoing email is recorded with its delivery result and whether the Sent-folder copy succeeded, so you can confirm mail is going out and spot errors at a glance. -- **Multiple named workflows**, each personalized: its own approval chain and the wording of all three emails (approval request, approved notice, denied notice) with `{placeholders}`. -- **Main page (login required):** users pick a workflow they have access to and submit a request from the browser. -- **Admin pages:** create/edit/pause workflows, monitor all requests and open their full logs, decide directly from the UI, manage users, and set which users can access which workflows. - -## Quick start (Docker) - -```bash -cp .env.example .env # edit it: SECRET_KEY, BASE_URL, MAIL_USER, MAIL_PASSWORD -docker compose up -d -``` - -Open http://localhost:8000 and sign in with `ADMIN_USERNAME` / `ADMIN_PASSWORD` from `.env` (defaults `admin` / `changeme` — change them). The SQLite database lives in `./data/`. - -> **If a value in `.env` contains a `$`** (common in passwords), escape it as `$$` — e.g. `MAIL_PASSWORD=xK9$$vh4pQ2`. Docker Compose otherwise treats `$word` as a variable and silently replaces it with a blank string, printing a warning like `The "word" variable is not set`. - -## Quick start (bare Python) - -```bash -pip install -r requirements.txt -cp .env.example .env -set -a; source .env; set +a # or export the variables another way -python app.py -``` - -Leaving `MAIL_USER` empty prints outgoing mail to the console instead of sending it — handy for trying the app before entering the account. - -## Mail server - -The app is preconfigured for Purelymail's servers — you never enter host or port settings: - -| | | -|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com`, port `465`, SSL/TLS | -| Sent-folder copy (IMAP) | `imap.purelymail.com`, port `993`, SSL/TLS | - -Every email the app sends is also saved as a copy to the account's **Sent** folder over IMAP, so the mailbox keeps a complete record of what went out. - -Only two settings are asked for in `.env`: `MAIL_USER` (the email address) and `MAIL_PASSWORD`. If the account uses Two Factor Authentication, use an **App Password** instead of the real password. - -## The request lifecycle - -``` -web form (login required) - │ received + logged - ▼ - approval request → approver 1 ── deny ──► STOP: DENIED notice to - │ approve everyone involved so far - ▼ - approval request → approver 2 ── deny ──► STOP: DENIED notice ... - │ approve - ▼ - ⋮ (… every approver in the chain, in order) - ▼ - last approver approves ──► APPROVED notice to everyone involved - (requester + all approvers) -``` - -Each step gets a fresh secret link, so an earlier approver's link cannot decide later steps. - -## Personalization placeholders - -Usable in every workflow's subject and body templates: -`{workflow}`, `{requester}`, `{subject}`, `{body}`, `{request_id}`, `{step}`, `{total_steps}`, `{decided_by}`, `{summary}` (creation + each approval with timestamps, for the approved notice), `{deny_reason}`, `{approve_url}`, `{deny_url}`. - -## Upgrading an existing instance (no data loss) - -All schema changes are applied automatically at boot by the built-in migrations — -the SQLite database and uploaded files live in `./data/`, which is a volume and is -never touched by rebuilding the image. - -```bash -cd /path/to/approvalflow -docker compose down # stop the app (data stays in ./data) -cp -r data data.backup-$(date +%F) # safety backup of DB + attachments -# replace the code with the new version, KEEPING .env and data/: -# unzip -o approvalflow.zip -d .. (or copy the new files over the old ones) -docker compose up -d --build # rebuild image, start; migrations run at boot -docker compose logs | grep migrate # optional: see what was migrated -``` - -Roll back = stop, restore `data.backup-…` to `data/`, start the previous image. - -## Security notes - -- Set a strong `SECRET_KEY` and put the app behind HTTPS (a reverse proxy such as Caddy or nginx) — the approve/deny links are secrets. -- Decision links are unguessable random tokens and work only once; a second click shows "already decided." -- Passwords are stored hashed (Werkzeug PBKDF2). Change the bootstrap admin password immediately. -- Non-admin users only ever see and use the workflows they've been granted. - -## Project layout - -``` -app.py Flask routes, auth, admin pages -db.py SQLite schema + audit/mail-log helpers -pipeline.py approval chain + decision/notification logic -mailer.py Purelymail SMTP sending, Sent-folder copy, mail log -templates/ pages static/style.css styling -``` diff --git a/Approval Workflow/app.py b/Approval Workflow/app.py deleted file mode 100644 index 22bd546..0000000 --- a/Approval Workflow/app.py +++ /dev/null @@ -1,579 +0,0 @@ -"""ApprovalFlow — self-hosted email approval workflows.""" -import functools -import json -import os -import re -import secrets - -from flask import (Flask, abort, flash, g, redirect, render_template, - request, send_file, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -import db -import pipeline -import totp -from mailer import send_mail -from db import DEFAULT_TEMPLATES - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) -# 15 MB attachment limit (+ small allowance for the rest of the form) -app.config["MAX_CONTENT_LENGTH"] = db.MAX_ATTACHMENT_BYTES + 512 * 1024 - - -@app.errorhandler(413) -def too_large(e): - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - return redirect(url_for("index")) - - -# ---------------------------------------------------------------- helpers - -@app.template_filter("fromjson") -def fromjson_filter(s): - try: - return json.loads(s or "[]") - except Exception: - return [] - - -def slugify(name): - s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") - return s or "workflow" - - -def login_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - return view(*a, **kw) - return wrapped - - -def admin_required(view): - @functools.wraps(view) - def wrapped(*a, **kw): - if "user_id" not in session: - return redirect(url_for("login", next=request.path)) - if not session.get("is_admin"): - abort(403) - return view(*a, **kw) - return wrapped - - -@app.before_request -def open_db(): - g.db = db.get_db() - - -@app.teardown_request -def close_db(exc): - d = g.pop("db", None) - if d: - d.close() - - -def user_workflows(user_id): - if session.get("is_admin"): - return g.db.execute("SELECT * FROM workflows WHERE active=1 ORDER BY name").fetchall() - return g.db.execute( - "SELECT w.* FROM workflows w JOIN user_workflows uw ON uw.workflow_id=w.id " - "WHERE uw.user_id=? AND w.active=1 ORDER BY w.name", (user_id,)).fetchall() - - -# ---------------------------------------------------------------- auth - -def _start_session(user, next_url=None): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - session["is_admin"] = bool(user["is_admin"]) - return redirect(next_url or url_for("index")) - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - user = g.db.execute("SELECT * FROM users WHERE username=?", - (request.form.get("username", "").strip(),)).fetchone() - if user and check_password_hash(user["password_hash"], request.form.get("password", "")): - if user["mfa_secret"]: - session.clear() - session["mfa_pending"] = user["id"] - session["mfa_next"] = request.args.get("next") or "" - return redirect(url_for("login_mfa")) - return _start_session(user, request.args.get("next")) - flash("Wrong username or password.", "error") - return render_template("login.html") - - -@app.route("/login/mfa", methods=["GET", "POST"]) -def login_mfa(): - uid = session.get("mfa_pending") - if not uid: - return redirect(url_for("login")) - user = g.db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() - if user is None or not user["mfa_secret"]: - session.clear() - return redirect(url_for("login")) - if request.method == "POST": - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - return _start_session(user, session.get("mfa_next") or None) - flash("Wrong verification code — try again.", "error") - return render_template("login.html", mfa_stage=True, username=user["username"]) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- account (MFA) - -@app.route("/account", methods=["GET", "POST"]) -@login_required -def account(): - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - setup_secret = session.get("mfa_setup") - - if request.method == "POST": - act = request.form.get("action") - if act == "profile": - email = request.form.get("email", "").strip().lower() - if email and "@" not in email: - flash("That e-mail address doesn't look valid.", "error") - else: - g.db.execute( - "UPDATE users SET first_name=?, last_name=?, unit_location=?, email=? WHERE id=?", - (request.form.get("first_name", "").strip(), - request.form.get("last_name", "").strip(), - request.form.get("unit_location", "").strip(), - email, user["id"])) - g.db.commit() - flash("Profile saved.", "ok") - return redirect(url_for("account")) - if act == "password": - if not check_password_hash(user["password_hash"], request.form.get("current_password", "")): - flash("Your current password is wrong.", "error") - elif not request.form.get("new_password"): - flash("The new password can't be empty.", "error") - elif request.form.get("new_password") != request.form.get("confirm_password"): - flash("The new passwords don't match.", "error") - else: - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(request.form["new_password"]), user["id"])) - g.db.commit() - flash("Password changed.", "ok") - return redirect(url_for("account")) - if act == "begin": - session["mfa_setup"] = totp.new_secret() - return redirect(url_for("account")) - if act == "cancel": - session.pop("mfa_setup", None) - return redirect(url_for("account")) - if act == "confirm" and setup_secret: - if totp.verify(setup_secret, request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret=? WHERE id=?", - (setup_secret, user["id"])) - g.db.commit() - session.pop("mfa_setup", None) - flash("Two-factor authentication is now enabled on your account.", "ok") - else: - flash("That code didn't match — scan the QR code and try again.", "error") - return redirect(url_for("account")) - if act == "disable" and user["mfa_secret"]: - if totp.verify(user["mfa_secret"], request.form.get("code", "")): - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", (user["id"],)) - g.db.commit() - flash("Two-factor authentication disabled.", "ok") - else: - flash("Wrong code — MFA was not disabled.", "error") - return redirect(url_for("account")) - - otpauth = totp.otpauth_uri(user["username"], setup_secret) if setup_secret else "" - return render_template("account.html", user=user, - setup_secret=setup_secret, otpauth=otpauth) - - -# ---------------------------------------------------------------- main page - -@app.route("/", methods=["GET", "POST"]) -@login_required -def index(): - workflows = user_workflows(session["user_id"]) - - user = g.db.execute("SELECT * FROM users WHERE id=?", (session["user_id"],)).fetchone() - - if request.method == "POST": - wf_id = request.form.get("workflow_id", type=int) - wf = next((w for w in workflows if w["id"] == wf_id), None) - if wf is None: - abort(403) # not allowed to use this workflow - requester = request.form.get("request_email", "").strip().lower() - subject = request.form.get("subject", "").strip() - body = request.form.get("body", "").strip() - uploads = [(f, f.read()) for f in request.files.getlist("attachments") - if f and f.filename] - total = sum(len(data) for _, data in uploads) - if not requester or "@" not in requester: - flash("Request's e-mail is required and must be a valid address.", "error") - elif not subject or not body: - flash("Subject and message are both required.", "error") - elif total > db.MAX_ATTACHMENT_BYTES: - flash("The uploaded files are too large — the combined limit is 15 MB.", "error") - else: - rid, _ = db.create_request(g.db, wf["id"], "web", requester, subject, body) - db.log(g.db, rid, "matched", f"Submitted on the web form by {session['username']}") - if uploads: - os.makedirs(db.ATTACH_DIR, exist_ok=True) - entries = [] - for i, (f, data) in enumerate(uploads, 1): - name = secure_filename(f.filename) or f"attachment-{i}" - stored = f"{rid}_{i}_{name}" - with open(db.attachment_path(stored), "wb") as fh: - fh.write(data) - entries.append({"stored": stored, "name": name}) - g.db.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entries), rid)) - db.log(g.db, rid, "attachments saved", - ", ".join(f"'{e['name']}'" for e in entries) + - f" ({total} bytes total) — included in every approval mail") - g.db.commit() - pipeline.send_approval_request(rid) - pipeline.notify_knowledge(rid, f"New request created by {requester}") - flash(f"Request #{rid} sent to the approver of “{wf['name']}”.", "ok") - return redirect(url_for("index")) - - my_requests = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "WHERE r.requester=? OR r.requester=? ORDER BY r.id DESC LIMIT 15", - (session["username"], (user["email"] or "-").lower())).fetchall() - return render_template("index.html", workflows=workflows, my_requests=my_requests, - user_email=user["email"] or "") - - -# ---------------------------------------------------------------- decision links (from email) - -@app.route("/decision//", methods=["GET", "POST"]) -def decision(token, action): - if action not in ("approve", "deny"): - abort(404) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.token=?", (token,)).fetchone() - if req is None: - return render_template("decision.html", stage="notfound"), 404 - - chain = pipeline.approver_chain(req) - approver = chain[min(req["current_step"], len(chain) - 1)] if chain else "" - if request.method == "POST": - decided_by = request.form.get("decided_by", "").strip() or approver - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain, - error="A reason for denial is required."), 400 - status, req2, message = pipeline.decide(token, action, decided_by, reason=reason) - return render_template("decision.html", stage="done", req=req2, message=message, - action=action, status=status) - return render_template("decision.html", stage="confirm", req=req, action=action, - approver=approver, chain=chain) - - -# ---------------------------------------------------------------- admin: dashboard & requests - -@app.route("/admin") -@admin_required -def admin(): - rows = g.db.execute( - "SELECT r.*, w.name wf_name FROM requests r JOIN workflows w ON w.id=r.workflow_id " - "ORDER BY r.id DESC LIMIT 200").fetchall() - stats = g.db.execute( - "SELECT status, COUNT(*) c FROM requests GROUP BY status").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - return render_template("admin.html", rows=rows, workflows=workflows, - stats={s["status"]: s["c"] for s in stats}) - - -@app.route("/admin/request/", methods=["GET", "POST"]) -@admin_required -def admin_request(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - if request.method == "POST" and req["status"] == "pending": - action = request.form.get("action") - reason = request.form.get("reason", "").strip() - if action == "deny" and not reason: - flash("A reason for denial is required.", "error") - return redirect(url_for("admin_request", rid=rid)) - if action in ("approve", "deny"): - pipeline.decide(req["token"], action, f"{session['username']} (admin)", reason=reason) - return redirect(url_for("admin_request", rid=rid)) - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - participants = json.loads(req["participants"]) - chain = pipeline.approver_chain(req) - return render_template("request_detail.html", req=req, logs=logs, - participants=participants, chain=chain, - attachments=pipeline.attachment_list(req)) - - -@app.route("/admin/request//attachment/") -@admin_required -def request_attachment(rid, idx): - req = g.db.execute("SELECT attachment FROM requests WHERE id=?", (rid,)).fetchone() - if req is None: - abort(404) - entries = pipeline.attachment_list(req) - if idx < 0 or idx >= len(entries): - abort(404) - path = db.attachment_path(entries[idx]["stored"]) - if not os.path.exists(path): - abort(404) - return send_file(path, as_attachment=True, download_name=entries[idx]["name"]) - - -@app.route("/admin/request//send-log", methods=["POST"]) -@admin_required -def send_request_log(rid): - req = g.db.execute("SELECT r.*, w.name wf_name, w.approvers, w.knowledge FROM requests r " - "JOIN workflows w ON w.id=r.workflow_id WHERE r.id=?", (rid,)).fetchone() - if req is None: - abort(404) - to = request.form.get("email", "").strip() - if not to or "@" not in to: - flash("A valid e-mail address is required to send the log.", "error") - return redirect(url_for("admin_request", rid=rid)) - - logs = g.db.execute("SELECT * FROM request_log WHERE request_id=? ORDER BY id", (rid,)).fetchall() - chain = pipeline.approver_chain(req) - participants = json.loads(req["participants"]) - - lines = [ - f"Full log of request #{req['id']} — {req['subject']}", - "=" * 60, - f"Workflow: {req['wf_name']}", - f"Requester: {req['requester']}", - f"Created: {req['created_at']}", - f"Status: {req['status'].upper()}", - ] - if req["decided_by"]: - lines.append(f"Final decision: by {req['decided_by']} at {req['decided_at']}") - if req["deny_reason"]: - lines.append(f"Denial reason: {req['deny_reason']}") - lines += [ - f"Approval chain: {' -> '.join(chain)}", - f"Knowledge chain: {', '.join(pipeline.knowledge_chain(req)) or '(none)'}", - f"People involved: {', '.join(participants)}", - "", - "Message:", - req["body"], - "", - "Event log:", - "-" * 60, - ] - for l in logs: - lines.append(f"{l['at']} | {l['event']}") - if l["detail"]: - lines.append(f"{'':25} {l['detail']}") - body = "\n".join(lines) - - ok, detail = send_mail(to, f"[{req['wf_name']}] Full log of request #{req['id']}: {req['subject']}", - body, request_id=rid) - db.log(g.db, rid, "log sent" if ok else "mail error", - f"Full log emailed to {to} by {session['username']} — {detail}") - g.db.commit() - flash(f"Full log sent to {to}." if ok else f"Could not send the log: {detail}", - "ok" if ok else "error") - return redirect(url_for("admin_request", rid=rid)) - - -# ---------------------------------------------------------------- admin: workflows - -WF_FIELDS = ("approval_subject", "approval_body", "approved_subject", - "approved_body", "denied_subject", "denied_body") - - -@app.route("/admin/workflows/new", methods=["GET", "POST"]) -@app.route("/admin/workflows/", methods=["GET", "POST"]) -@admin_required -def workflow_edit(wid=None): - wf = g.db.execute("SELECT * FROM workflows WHERE id=?", (wid,)).fetchone() if wid else None - if wid and wf is None: - abort(404) - - if request.method == "POST": - f = request.form - name = f.get("name", "").strip() - approvers = [a.strip() for a in f.get("approvers", "").splitlines() if a.strip()] - knowledge = [a.strip() for a in f.get("knowledge", "").splitlines() if a.strip()] - if not name or not approvers: - flash("A name and at least one approver are required.", "error") - else: - templates = {k: f.get(k, "").strip() or DEFAULT_TEMPLATES[k] for k in WF_FIELDS} - try: - if wf is None: - g.db.execute( - "INSERT INTO workflows (name, slug, description, approvers, knowledge, active," - " approval_subject, approval_body, approved_subject, approved_body," - " denied_subject, denied_body, created_at)" - " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], db.now())) - flash(f"Workflow “{name}” created with {len(approvers)} approval step(s).", "ok") - else: - g.db.execute( - "UPDATE workflows SET name=?, slug=?, description=?, approvers=?, knowledge=?," - " active=?, approval_subject=?, approval_body=?," - " approved_subject=?, approved_body=?, denied_subject=?, denied_body=? WHERE id=?", - (name, slugify(name), f.get("description", "").strip(), json.dumps(approvers), - json.dumps(knowledge), 1 if f.get("active") else 0, - *[templates[k] for k in WF_FIELDS], wid)) - flash(f"Workflow “{name}” saved.", "ok") - g.db.commit() - return redirect(url_for("admin")) - except Exception as e: - flash(f"Could not save: {e}", "error") - return render_template("workflow_edit.html", wf=wf, defaults=DEFAULT_TEMPLATES, - images=_email_images()) - - -# ---------------------------------------------------------------- admin: mail status - -@app.route("/admin/mail") -@admin_required -def mail_status(): - rows = g.db.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 300").fetchall() - stats = {s["status"]: s["c"] for s in - g.db.execute("SELECT status, COUNT(*) c FROM mail_log GROUP BY status")} - copy_fails = g.db.execute( - "SELECT COUNT(*) c FROM mail_log WHERE sent_copy LIKE 'failed%'").fetchone()["c"] - configured = bool(os.environ.get("MAIL_USER")) - return render_template("mail_status.html", rows=rows, stats=stats, - copy_fails=copy_fails, configured=configured) - - -# ---------------------------------------------------------------- admin: email images - -def _email_images(): - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - return sorted(f for f in os.listdir(db.EMAIL_IMG_DIR) - if os.path.splitext(f)[1].lower() in db.EMAIL_IMAGE_TYPES) - - -@app.route("/admin/email-images/upload", methods=["POST"]) -@admin_required -def email_image_upload(): - nxt = request.form.get("next") or url_for("admin") - upload = request.files.get("image") - if not upload or not upload.filename: - flash("Choose an image file to upload.", "error") - return redirect(nxt) - name = secure_filename(upload.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in db.EMAIL_IMAGE_TYPES: - flash("Only PNG, JPG and GIF images can be used in emails.", "error") - return redirect(nxt) - data = upload.read() - if len(data) > db.MAX_EMAIL_IMAGE_BYTES: - flash("Email images are limited to 2 MB each.", "error") - return redirect(nxt) - os.makedirs(db.EMAIL_IMG_DIR, exist_ok=True) - if os.path.exists(os.path.join(db.EMAIL_IMG_DIR, name)): - flash(f"An image named “{name}” already exists — delete it first or rename the file.", "error") - return redirect(nxt) - with open(os.path.join(db.EMAIL_IMG_DIR, name), "wb") as fh: - fh.write(data) - flash(f"Image “{name}” uploaded — use it with {{image:{name}}} in the email bodies.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/delete", methods=["POST"]) -@admin_required -def email_image_delete(): - nxt = request.form.get("next") or url_for("admin") - name = secure_filename(request.form.get("name", "")) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if name and os.path.isfile(path): - os.remove(path) - flash(f"Image “{name}” deleted.", "ok") - return redirect(nxt) - - -@app.route("/admin/email-images/") -@admin_required -def email_image(name): - name = secure_filename(name) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if not (name and os.path.isfile(path)): - abort(404) - return send_file(path) - - -# ---------------------------------------------------------------- admin: users & access - -@app.route("/admin/users", methods=["GET", "POST"]) -@admin_required -def users(): - if request.method == "POST": - f = request.form - act = f.get("action") - try: - if act == "create": - if not f.get("username") or not f.get("password"): - flash("Username and password are required.", "error") - else: - g.db.execute( - "INSERT INTO users (username, password_hash, email, is_admin, created_at) VALUES (?,?,?,?,?)", - (f["username"].strip(), generate_password_hash(f["password"]), - f.get("email", "").strip(), 1 if f.get("is_admin") else 0, db.now())) - flash(f"User “{f['username']}” created.", "ok") - elif act == "delete": - uid = f.get("user_id", type=int) - if uid == session["user_id"]: - flash("You can't delete your own account.", "error") - else: - g.db.execute("DELETE FROM users WHERE id=?", (uid,)) - flash("User deleted.", "ok") - elif act == "mfa_off": - g.db.execute("UPDATE users SET mfa_secret='' WHERE id=?", - (f.get("user_id", type=int),)) - flash("MFA disabled for that user — they can re-enable it from their Account page.", "ok") - elif act == "password": - g.db.execute("UPDATE users SET password_hash=? WHERE id=?", - (generate_password_hash(f["password"]), f.get("user_id", type=int))) - flash("Password updated.", "ok") - elif act == "access": - uid = f.get("user_id", type=int) - g.db.execute("DELETE FROM user_workflows WHERE user_id=?", (uid,)) - for wid in request.form.getlist("workflow_ids", type=int): - g.db.execute("INSERT OR IGNORE INTO user_workflows (user_id, workflow_id) VALUES (?,?)", - (uid, wid)) - flash("Access updated.", "ok") - g.db.commit() - except Exception as e: - flash(f"Error: {e}", "error") - return redirect(url_for("users")) - - all_users = g.db.execute("SELECT * FROM users ORDER BY username").fetchall() - workflows = g.db.execute("SELECT * FROM workflows ORDER BY name").fetchall() - access = {} - for row in g.db.execute("SELECT * FROM user_workflows"): - access.setdefault(row["user_id"], set()).add(row["workflow_id"]) - return render_template("users.html", users=all_users, workflows=workflows, access=access) - - -# ---------------------------------------------------------------- boot - -db.init_db() - -if __name__ == "__main__": - app.run(host=os.environ.get("HOST", "0.0.0.0"), - port=int(os.environ.get("PORT", "8000")), - debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/Approval Workflow/db.py b/Approval Workflow/db.py deleted file mode 100644 index 145b578..0000000 --- a/Approval Workflow/db.py +++ /dev/null @@ -1,249 +0,0 @@ -"""SQLite data layer for ApprovalFlow.""" -import json -import os -import secrets -import sqlite3 -from datetime import datetime, timezone - -from werkzeug.security import generate_password_hash - -DB_PATH = os.environ.get("DATABASE_PATH", os.path.join(os.path.dirname(__file__), "data", "approvalflow.db")) -ATTACH_DIR = os.path.join(os.path.dirname(DB_PATH), "attachments") -EMAIL_IMG_DIR = os.path.join(os.path.dirname(DB_PATH), "email_images") - -MAX_EMAIL_IMAGE_BYTES = 2 * 1024 * 1024 # 2 MB per email image -EMAIL_IMAGE_TYPES = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif"} - -MAX_ATTACHMENT_BYTES = 15 * 1024 * 1024 # 15 MB - - -def attachment_path(stored_name): - return os.path.join(ATTACH_DIR, stored_name) - -SCHEMA = """ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT DEFAULT '', - first_name TEXT NOT NULL DEFAULT '', - last_name TEXT NOT NULL DEFAULT '', - unit_location TEXT NOT NULL DEFAULT '', - mfa_secret TEXT NOT NULL DEFAULT '', -- empty = MFA disabled - is_admin INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS workflows ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - slug TEXT UNIQUE NOT NULL, - description TEXT DEFAULT '', - approvers TEXT NOT NULL DEFAULT '[]', -- JSON list, in approval order - knowledge TEXT NOT NULL DEFAULT '[]', -- JSON list: informed of every event, never approve - active INTEGER NOT NULL DEFAULT 1, - -- personalization (templates support {placeholders}) - approval_subject TEXT NOT NULL, - approval_body TEXT NOT NULL, - approved_subject TEXT NOT NULL, - approved_body TEXT NOT NULL, - denied_subject TEXT NOT NULL, - denied_body TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS user_workflows ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - workflow_id INTEGER NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, workflow_id) -); - -CREATE TABLE IF NOT EXISTS requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - workflow_id INTEGER NOT NULL REFERENCES workflows(id), - source TEXT NOT NULL DEFAULT 'web', -- requests are created on the site - requester TEXT NOT NULL, -- email address (or username for web) - subject TEXT NOT NULL, - body TEXT NOT NULL, - participants TEXT NOT NULL DEFAULT '[]', -- JSON list of everyone involved so far - status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / denied - current_step INTEGER NOT NULL DEFAULT 0, -- index into the workflow's approver chain - token TEXT UNIQUE NOT NULL, -- secret for the CURRENT step's approve/deny links - decided_by TEXT DEFAULT '', - deny_reason TEXT NOT NULL DEFAULT '', - attachment TEXT NOT NULL DEFAULT '[]', -- JSON list of {"stored": ..., "name": ...} - created_at TEXT NOT NULL, - decided_at TEXT -); - -CREATE TABLE IF NOT EXISTS decisions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER NOT NULL REFERENCES requests(id) ON DELETE CASCADE, - step INTEGER NOT NULL, -- 1-based position in the chain - decided_by TEXT NOT NULL, - decision TEXT NOT NULL, -- approved / denied - at TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS request_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE, - at TEXT NOT NULL, - event TEXT NOT NULL, - detail TEXT DEFAULT '' -); - -CREATE TABLE IF NOT EXISTS mail_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - at TEXT NOT NULL, - request_id INTEGER, -- related request, if any - recipients TEXT NOT NULL, - subject TEXT NOT NULL, - status TEXT NOT NULL, -- sent / error / dev - detail TEXT DEFAULT '', -- SMTP error message, if any - sent_copy TEXT DEFAULT '' -- result of saving to the Sent folder -); -""" - -DEFAULT_TEMPLATES = { - "approval_subject": "[{workflow}] Approval needed ({step}/{total_steps}): {subject}", - "approval_body": ( - "A request needs your decision (approval step {step} of {total_steps}).\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Approve: {approve_url}\n" - "Deny: {deny_url}\n\n" - "If you approve, the request moves on to the next approver.\n" - "If you deny, the workflow stops and everyone involved is notified.\n" - ), - "approved_subject": "[{workflow}] Approved: {subject}", - "approved_body": ( - "The following request was APPROVED.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Summary:\n{summary}\n" - ), - "denied_subject": "[{workflow}] Denied: {subject}", - "denied_body": ( - "The following request was DENIED by {decided_by}.\n\n" - "Workflow: {workflow}\n" - "From: {requester}\n" - "Created: {created_at}\n" - "Subject: {subject}\n\n" - "Message:\n{body}\n\n" - "Reason for denial:\n{deny_reason}\n" - ), -} - - -def now(): - return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - - -def get_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - con = sqlite3.connect(DB_PATH, timeout=15) - con.row_factory = sqlite3.Row - con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") - con.execute("PRAGMA busy_timeout = 15000") - return con - - -def _migrate(con): - """Upgrade a database created by the single-approver version, if present.""" - cols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if "approver_email" in cols and "approvers" not in cols: - con.execute("ALTER TABLE workflows ADD COLUMN approvers TEXT NOT NULL DEFAULT '[]'") - for w in con.execute("SELECT id, approver_email FROM workflows").fetchall(): - con.execute("UPDATE workflows SET approvers=? WHERE id=?", - (json.dumps([w["approver_email"]]), w["id"])) - print("[migrate] Converted single approver_email to approver chains.") - rcols = {r["name"] for r in con.execute("PRAGMA table_info(requests)")} - if rcols and "current_step" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN current_step INTEGER NOT NULL DEFAULT 0") - print("[migrate] Added current_step to requests.") - if rcols and "deny_reason" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN deny_reason TEXT NOT NULL DEFAULT ''") - print("[migrate] Added deny_reason to requests.") - if rcols and "attachment" not in rcols: - con.execute("ALTER TABLE requests ADD COLUMN attachment TEXT NOT NULL DEFAULT ''") - print("[migrate] Added attachment to requests.") - ucols = {r["name"] for r in con.execute("PRAGMA table_info(users)")} - if ucols and "mfa_secret" not in ucols: - con.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT NOT NULL DEFAULT ''") - print("[migrate] Added mfa_secret to users.") - for col in ("first_name", "last_name", "unit_location"): - if ucols and col not in ucols: - con.execute(f"ALTER TABLE users ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") - print(f"[migrate] Added {col} to users.") - wcols = {r["name"] for r in con.execute("PRAGMA table_info(workflows)")} - if wcols and "knowledge" not in wcols: - con.execute("ALTER TABLE workflows ADD COLUMN knowledge TEXT NOT NULL DEFAULT '[]'") - print("[migrate] Added knowledge chain to workflows.") - # single-attachment values -> JSON list format - if rcols and "attachment" in rcols: - for row in con.execute("SELECT id, attachment FROM requests " - "WHERE attachment != '' AND attachment NOT LIKE '[%'").fetchall(): - stored = row["attachment"] - entry = [{"stored": stored, "name": stored.split("_", 1)[-1]}] - con.execute("UPDATE requests SET attachment=? WHERE id=?", - (json.dumps(entry), row["id"])) - print(f"[migrate] Converted attachment of request #{row['id']} to list format.") - con.execute("UPDATE requests SET attachment='[]' WHERE attachment=''") - con.commit() - - -def init_db(): - con = get_db() - _migrate(con) - con.executescript(SCHEMA) - # bootstrap admin user on first run - if con.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - username = os.environ.get("ADMIN_USERNAME", "admin") - password = os.environ.get("ADMIN_PASSWORD", "changeme") - con.execute( - "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?,?,1,?)", - (username, generate_password_hash(password), now()), - ) - print(f"[init] Created admin user '{username}'. Change the password immediately.") - con.commit() - con.close() - - -def log(con, request_id, event, detail=""): - con.execute( - "INSERT INTO request_log (request_id, at, event, detail) VALUES (?,?,?,?)", - (request_id, now(), event, detail), - ) - - -def add_participants(con, request_id, emails): - """Add people to the 'involved so far' list of a request.""" - row = con.execute("SELECT participants FROM requests WHERE id=?", (request_id,)).fetchone() - current = json.loads(row["participants"]) - for e in emails: - e = (e or "").strip().lower() - if e and e not in current: - current.append(e) - con.execute("UPDATE requests SET participants=? WHERE id=?", (json.dumps(current), request_id)) - return current - - -def create_request(con, workflow_id, source, requester, subject, body, extra_participants=()): - token = secrets.token_urlsafe(32) - cur = con.execute( - "INSERT INTO requests (workflow_id, source, requester, subject, body, token, created_at) " - "VALUES (?,?,?,?,?,?,?)", - (workflow_id, source, requester, subject, body, token, now()), - ) - rid = cur.lastrowid - add_participants(con, rid, [requester, *extra_participants]) - log(con, rid, "received", f"Request received via {source} from {requester}") - return rid, token diff --git a/Approval Workflow/docker-compose.yml b/Approval Workflow/docker-compose.yml deleted file mode 100644 index 32e4353..0000000 --- a/Approval Workflow/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - approvalflow: - build: . - ports: - - "8000:8000" - env_file: .env - volumes: - - ./data:/data - restart: unless-stopped diff --git a/Approval Workflow/mailer.py b/Approval Workflow/mailer.py deleted file mode 100644 index 51c7fc9..0000000 --- a/Approval Workflow/mailer.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Outbound email via Purelymail, and template rendering. - -Preconfigured for Purelymail — only the account's login and password are needed: - - MAIL_USER=you@yourdomain.com (or you@purelymail.com) - MAIL_PASSWORD=... (use an App Password if 2FA is enabled) - -Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a -copy is saved to the account's Sent folder over IMAP -(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is -recorded in the mail_log table shown on the admin "Mail status" page. -""" -import html as html_mod -import imaplib -import os -import smtplib -import time -from email.message import EmailMessage -from email.utils import make_msgid - -import db - -SMTP_HOST = "smtp.purelymail.com" -SMTP_PORT = 465 # SSL/TLS -IMAP_HOST = "imap.purelymail.com" -IMAP_PORT = 993 # SSL/TLS - -SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items") - - -def render(template: str, **vars) -> str: - """Safe {placeholder} substitution — unknown placeholders are left as-is.""" - out = template - for k, v in vars.items(): - out = out.replace("{" + k + "}", str(v)) - return out - - -def _save_to_sent(user, password, msg) -> str: - """Append a copy of the message to the account's Sent folder. Returns a status string.""" - try: - with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap: - imap.login(user, password) - raw = msg.as_bytes() - stamp = imaplib.Time2Internaldate(time.time()) - last_err = "" - for folder in SENT_FOLDER_CANDIDATES: - status, resp = imap.append(folder, r"(\Seen)", stamp, raw) - if status == "OK": - return f"saved to '{folder}'" - last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else "" - # No known folder worked — create "Sent" and try once more. - imap.create("Sent") - status, resp = imap.append("Sent", r"(\Seen)", stamp, raw) - if status == "OK": - return "saved to 'Sent' (folder created)" - return f"failed: {last_err or 'could not append to any Sent folder'}" - except Exception as e: - return f"failed: {e}" - - -def _record(request_id, recipients, subject, status, detail, sent_copy): - try: - con = db.get_db() - con.execute( - "INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)" - " VALUES (?,?,?,?,?,?,?)", - (db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy)) - con.commit() - con.close() - except Exception as e: - print(f"[mail] Could not record mail log entry: {e}") - - -def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None): - """Send an email through Purelymail, keep a copy in the Sent folder, and - record the attempt in the mail log. Returns (ok, detail). - - attachments: optional list of (filename, bytes, maintype, subtype). - inline_images: optional dict {marker_name: (bytes, subtype)}. When given, - an HTML alternative is built where each "{image:marker_name}" - in the body becomes an embedded ; the plain-text part - shows "[image: marker_name]" instead. - """ - if isinstance(to_addrs, str): - to_addrs = [to_addrs] - to_addrs = [a for a in to_addrs if a] - if not to_addrs: - return False, "no recipients" - - user = os.environ.get("MAIL_USER", "") - password = os.environ.get("MAIL_PASSWORD", "") - - msg = EmailMessage() - msg["From"] = user or "approvalflow@localhost" - msg["To"] = ", ".join(to_addrs) - msg["Subject"] = subject - - if inline_images: - text = body - for name in inline_images: - text = text.replace("{image:" + name + "}", f"[image: {name}]") - msg.set_content(text) - - html = html_mod.escape(body).replace("\n", "
\n") - cids = {} - for name in inline_images: - cid = make_msgid(domain="martinhal.approvalflow") - cids[name] = cid - html = html.replace("{image:" + name + "}", - f'{html_mod.escape(name)}') - msg.add_alternative( - f'
{html}
', - subtype="html") - html_part = msg.get_payload()[-1] - for name, (data, subtype) in inline_images.items(): - html_part.add_related(data, maintype="image", subtype=subtype, cid=cids[name]) - else: - msg.set_content(body) - - for filename, data, maintype, subtype in (attachments or []): - msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename) - - if not user: - # Dev mode: print instead of sending so the app is testable without an account. - att = "".join(f"\n[attachment: {f} ({len(b)} bytes)]" for f, b, *_ in (attachments or [])) - img = "".join(f"\n[inline image: {n} ({len(d)} bytes, {s})]" for n, (d, s) in (inline_images or {}).items()) - print(f"[mail:DEV] To: {msg['To']}\nSubject: {subject}\n{body}{att}{img}\n{'-'*50}") - _record(request_id, to_addrs, subject, "dev", - "MAIL_USER not set — printed to console", "skipped (dev mode)") - return True, "dev mode (MAIL_USER not set) — printed to console" - - try: - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server: - server.login(user, password) - server.send_message(msg) - except Exception as e: - detail = f"SMTP error: {e}" - _record(request_id, to_addrs, subject, "error", detail, "skipped (send failed)") - return False, detail - - sent_copy = _save_to_sent(user, password, msg) - _record(request_id, to_addrs, subject, "sent", "", sent_copy) - return True, f"sent to {', '.join(to_addrs)}" diff --git a/Approval Workflow/pipeline.py b/Approval Workflow/pipeline.py deleted file mode 100644 index 8db535a..0000000 --- a/Approval Workflow/pipeline.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Request pipeline for requests created on the site. - -Each workflow has an ordered chain of approvers. A request moves through the -chain one approver at a time: - - - DENY at any step -> the workflow STOPS; everyone involved so far gets - the denied notice. - - APPROVE -> the request moves to the next approver in the chain. - - APPROVE (last) -> the request is fully approved; everyone involved so - far gets the approved notice. - -Each step gets a fresh secret token, so an earlier approver's links cannot -decide later steps. -""" -import json -import mimetypes -import os -import secrets - -import db -from mailer import render, send_mail - -BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000").rstrip("/") - - -def approver_chain(wf): - try: - chain = json.loads(wf["approvers"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def _vars(wf, req, **extra): - chain = approver_chain(wf) - v = { - "workflow": wf["name"], - "requester": req["requester"], - "subject": req["subject"], - "body": req["body"], - "request_id": req["id"], - "created_at": req["created_at"], - "step": min(req["current_step"], max(len(chain) - 1, 0)) + 1, - "total_steps": len(chain), - "approve_url": f"{BASE_URL}/decision/{req['token']}/approve", - "deny_url": f"{BASE_URL}/decision/{req['token']}/deny", - } - v.update(extra) - return v - - -def _record_decision(con, request_id, step, decided_by, decision): - con.execute("INSERT INTO decisions (request_id, step, decided_by, decision, at) VALUES (?,?,?,?,?)", - (request_id, step, decided_by, decision, db.now())) - - -def build_summary(con, req, total_steps): - """Human-readable history: when the request was created and who - approved (or denied) each step, with timestamps.""" - lines = [f" Created {req['created_at']} by {req['requester']}"] - for d in con.execute("SELECT * FROM decisions WHERE request_id=? ORDER BY id", (req["id"],)): - verb = "Approved" if d["decision"] == "approved" else "DENIED" - lines.append(f" Step {d['step']}/{total_steps} {verb} {d['at']} by {d['decided_by']}") - return "\n".join(lines) - - -def collect_inline_images(rendered_body): - """Find {image:name} markers in a rendered body and load those images - from the email image library. Returns a dict for send_mail, or None.""" - import re - names = set(re.findall(r"\{image:([A-Za-z0-9._\-]+)\}", rendered_body)) - images = {} - for name in names: - ext = os.path.splitext(name)[1].lower() - subtype = db.EMAIL_IMAGE_TYPES.get(ext) - path = os.path.join(db.EMAIL_IMG_DIR, name) - if subtype and os.path.isfile(path): - with open(path, "rb") as f: - images[name] = (f.read(), subtype) - return images or None - - -def attachment_list(req): - """Parse the request's attachment column into [{"stored":…, "name":…}, …].""" - try: - return json.loads(req["attachment"] or "[]") - except Exception: - return [] - - -def load_attachments(req): - """Load every uploaded file of the request as send_mail attachments.""" - out = [] - for entry in attachment_list(req): - path = db.attachment_path(entry["stored"]) - try: - with open(path, "rb") as f: - data = f.read() - except OSError: - continue - ctype = mimetypes.guess_type(entry["name"])[0] or "application/octet-stream" - maintype, subtype = ctype.split("/", 1) - out.append((entry["name"], data, maintype, subtype)) - return out or None - - -def knowledge_chain(wf): - try: - chain = json.loads(wf["knowledge"]) - except Exception: - chain = [] - return [a.strip() for a in chain if a and a.strip()] - - -def notify_knowledge(request_id, event): - """Inform the workflow's knowledge chain of an event. These addresses are - never part of the approval chain — they only receive FYI messages.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - watchers = knowledge_chain(wf) - if not watchers: - return - chain = approver_chain(wf) - subject = f"[{wf['name']}] FYI — request #{req['id']}: {event}" - body = ( - f"For your information (you are on the knowledge chain of this workflow —\n" - f"no action is needed from you):\n\n" - f"{event}\n\n" - f"Workflow: {wf['name']}\n" - f"From: {req['requester']}\n" - f"Created: {req['created_at']}\n" - f"Subject: {req['subject']}\n" - f"Status: {req['status'].upper()}" - f" (step {min(req['current_step'], max(len(chain)-1, 0)) + 1} of {len(chain)})\n\n" - f"Message:\n{req['body']}\n" - ) - if req["deny_reason"]: - body += f"\nReason for denial:\n{req['deny_reason']}\n" - ok, detail = send_mail(watchers, subject, body, request_id=request_id, - attachments=load_attachments(req)) - db.log(con, request_id, - "knowledge chain informed" if ok else "mail error", - f"'{event}' to {', '.join(watchers)} — {detail}") - con.commit() - finally: - con.close() - - -def current_approver(wf, req): - chain = approver_chain(wf) - if not chain: - return None - return chain[min(req["current_step"], len(chain) - 1)] - - -def send_approval_request(request_id): - """Send the approval request to the approver of the CURRENT step.""" - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone() - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - approver = current_approver(wf, req) - if approver is None: - db.log(con, request_id, "error", f"Workflow '{wf['name']}' has no approvers configured") - con.commit() - return False - con.commit() # release any pending writes before slow mail I/O - v = _vars(wf, req) - body_t = wf["approval_body"] - if "{created_at}" not in body_t: # creation time is mandatory in every mail - body_t = "Request created: {created_at}\n\n" + body_t - attachments = load_attachments(req) - rendered_body = render(body_t, **v) - ok, detail = send_mail(approver, - render(wf["approval_subject"], **v), - rendered_body, - request_id=request_id, - attachments=attachments, - inline_images=collect_inline_images(rendered_body)) - db.add_participants(con, request_id, [approver]) - db.log(con, request_id, - "approval requested" if ok else "mail error", - f"Step {req['current_step'] + 1}/{len(chain)}: approval request to {approver} — {detail}") - con.commit() - return ok - finally: - con.close() - - -def _notify_outcome(con, wf, req, new_status, decided_by): - participants = db.add_participants(con, req["id"], [decided_by]) - summary = build_summary(con, req, len(approver_chain(wf))) - con.commit() # release the write lock before slow mail I/O (mail_log uses its own connection) - v = _vars(wf, req, decided_by=decided_by, decision=new_status.upper(), - summary=summary, deny_reason=req["deny_reason"]) - subj_t = wf["approved_subject"] if new_status == "approved" else wf["denied_subject"] - body_t = wf["approved_body"] if new_status == "approved" else wf["denied_body"] - # These are mandatory in the outcome notice: append them if the workflow's - # custom template doesn't include the placeholder. - if new_status == "approved" and "{summary}" not in body_t: - body_t += "\n\nSummary:\n{summary}\n" - if new_status == "denied" and "{deny_reason}" not in body_t: - body_t += "\n\nReason for denial:\n{deny_reason}\n" - if "{created_at}" not in body_t and "{summary}" not in body_t: - # creation time is mandatory in every mail ({summary} already includes it) - body_t = "Request created: {created_at}\n\n" + body_t - rendered_body = render(body_t, **v) - ok, detail = send_mail(participants, render(subj_t, **v), rendered_body, - request_id=req["id"], - inline_images=collect_inline_images(rendered_body)) - db.log(con, req["id"], - "participants notified" if ok else "mail error", - f"{new_status.capitalize()} notice to {', '.join(participants)} — {detail}") - - -def decide(token, action, decided_by, reason=""): - """Handle a decision at the request's current step. - - A denial requires a reason (callers enforce it as a mandatory field; - this is a second line of defense). - - Returns (status, request_row_or_None, message). - """ - assert action in ("approve", "deny") - reason = (reason or "").strip() - con = db.get_db() - try: - req = con.execute("SELECT * FROM requests WHERE token=?", (token,)).fetchone() - if req is None: - return "notfound", None, "This link is not valid — it may belong to a step that was already decided." - if req["status"] != "pending": - return "already", req, f"This request was already {req['status']}." - - wf = con.execute("SELECT * FROM workflows WHERE id=?", (req["workflow_id"],)).fetchone() - chain = approver_chain(wf) - step = req["current_step"] - - if action == "deny": - if not reason: - return "needreason", req, "A reason for denial is required." - # STOP the workflow, notify everyone involved so far. - con.execute("UPDATE requests SET status='denied', decided_by=?, decided_at=?, deny_reason=? WHERE id=?", - (decided_by, db.now(), reason, req["id"])) - _record_decision(con, req["id"], step + 1, decided_by, "denied") - db.log(con, req["id"], "denied", - f"Denied at step {step + 1}/{len(chain)} by {decided_by} — workflow stopped. Reason: {reason}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "denied", decided_by) - con.commit() - notify_knowledge(req["id"], f"DENIED at step {step + 1}/{len(chain)} by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} denied at step {step + 1} of {len(chain)}. " - "The workflow has stopped and everyone involved has been notified.") - - # approve - _record_decision(con, req["id"], step + 1, decided_by, "approved") - db.log(con, req["id"], "step approved", - f"Step {step + 1}/{len(chain)} approved by {decided_by}") - db.add_participants(con, req["id"], [decided_by]) - con.commit() - - if step + 1 >= len(chain): - # Last approver — fully approved. - con.execute("UPDATE requests SET status='approved', decided_by=?, decided_at=? WHERE id=?", - (decided_by, db.now(), req["id"])) - db.log(con, req["id"], "approved", - f"Final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - _notify_outcome(con, wf, req, "approved", decided_by) - con.commit() - notify_knowledge(req["id"], - f"FULLY APPROVED — final approval ({len(chain)}/{len(chain)}) by {decided_by}") - req = con.execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Request #{req['id']} fully approved ({len(chain)} of {len(chain)} steps). " - "Everyone involved has been notified.") - - # Move to the next approver with a fresh token (old links stop working). - new_token = secrets.token_urlsafe(32) - con.execute("UPDATE requests SET current_step=?, token=? WHERE id=?", - (step + 1, new_token, req["id"])) - con.commit() - notify_knowledge(req["id"], f"Step {step + 1}/{len(chain)} approved by {decided_by} — " - f"now waiting on {chain[step + 1]}") - send_approval_request(req["id"]) - req = db.get_db().execute("SELECT * FROM requests WHERE id=?", (req["id"],)).fetchone() - return "ok", req, (f"Step {step + 1} of {len(chain)} approved. The request was sent to the " - f"next approver ({chain[step + 1]}).") - finally: - con.close() diff --git a/Approval Workflow/requirements.txt b/Approval Workflow/requirements.txt deleted file mode 100644 index be005c9..0000000 --- a/Approval Workflow/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -werkzeug>=3.0 -gunicorn>=21.0 diff --git a/Approval Workflow/static/style.css b/Approval Workflow/static/style.css deleted file mode 100644 index 73489e0..0000000 --- a/Approval Workflow/static/style.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ApprovalFlow — cool paper + ink, rubber-stamp statuses, mono ledger */ -:root, [data-theme="light"] { - --paper: #fafbfc; - --card: #ffffff; - --field: #ffffff; - --hover: #f2f6f8; - --ink: #16232e; - --ink-soft: #5a6b78; - --line: #dfe5ea; - --approve: #0e7c66; - --deny: #b3372f; - --pending: #b7791f; - --focus: #2563a8; - --flash-ok-bg: #e4f3ee; --flash-ok-line: #bfe0d5; --flash-ok-ink: #0b5c4c; - --flash-err-bg: #f9e8e6; --flash-err-line: #edc7c3; --flash-err-ink: #872a24; - --radius: 8px; - font-size: 16px; -} -[data-theme="dark"] { - --paper: #10161c; - --card: #18212a; - --field: #10161c; - --hover: #1e2a35; - --ink: #e4ebf1; - --ink-soft: #93a5b2; - --line: #2b3945; - --approve: #35b598; - --deny: #e07067; - --pending: #d9a24a; - --focus: #6aa9e0; - --flash-ok-bg: #12352c; --flash-ok-line: #1d5a4a; --flash-ok-ink: #8fd8c3; - --flash-err-bg: #3c1f1c; --flash-err-line: #6b3530; --flash-err-ink: #f0b1aa; -} -[data-theme="dark"] body { color-scheme: dark; } -[data-theme="dark"] button, [data-theme="dark"] .btn { - border-color: var(--line); background: #26323e; -} -[data-theme="dark"] button:hover, [data-theme="dark"] .btn:hover { background: #31404e; } -[data-theme="dark"] .btn.approve { background: #14523f; border-color: #14523f; } -[data-theme="dark"] .btn.deny { background: #79312a; border-color: #79312a; } -[data-theme="dark"] .btn.ghost { background: transparent; color: var(--ink); } -* { box-sizing: border-box; } -body { - margin: 0; background: var(--paper); color: var(--ink); - font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.55; -} -.mono, code, .ledger, .stamp, .chip { font-family: "IBM Plex Mono", ui-monospace, monospace; } -a { color: var(--focus); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } - -/* top bar */ -.topbar { - display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; - padding: .8rem 1.5rem; background: var(--ink); color: #e9eef2; -} -.brand { font-weight: 700; font-size: 1.05rem; color: #fff; letter-spacing: .02em; } -.brand span { color: #7fd6c2; } -.topbar nav { display: flex; gap: 1.2rem; flex: 1; } -.topbar nav a { color: #b9c6cf; padding: .2rem 0; border-bottom: 2px solid transparent; } -.topbar nav a.on, .topbar nav a:hover { color: #fff; border-bottom-color: #7fd6c2; text-decoration: none; } -.who { display: flex; gap: 1rem; align-items: center; font-size: .9rem; } -.who a { color: #b9c6cf; } - -.wrap { max-width: 980px; margin: 2rem auto 4rem; padding: 0 1.25rem; } - -h1 { font-size: 1.6rem; margin: 0 0 .3rem; } -h2 { font-size: 1.15rem; margin: 2rem 0 .8rem; } -.sub { color: var(--ink-soft); margin: 0 0 1.6rem; } - -.card { - background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: 1.4rem 1.5rem; margin-bottom: 1.4rem; -} - -/* forms */ -label { display: block; font-weight: 600; font-size: .85rem; margin: 1rem 0 .3rem; } -label small { font-weight: 400; color: var(--ink-soft); } -input[type=text], input[type=password], input[type=email], select, textarea, input[type=file] { - width: 100%; padding: .55rem .7rem; font: inherit; color: var(--ink); - border: 1px solid var(--line); border-radius: 6px; background: var(--field); -} -input[type=file] { padding: .4rem .5rem; } -textarea { min-height: 130px; resize: vertical; } -textarea.tpl { font-family: "IBM Plex Mono", monospace; font-size: .85rem; min-height: 110px; } -.check { display: flex; gap: .5rem; align-items: center; font-weight: 400; margin-top: 1rem; } -.check input { width: auto; } - -button, .btn { - display: inline-block; margin-top: 1.1rem; padding: .55rem 1.3rem; - font: inherit; font-weight: 600; border-radius: 6px; cursor: pointer; - border: 1px solid var(--ink); background: var(--ink); color: #fff; -} -button:hover, .btn:hover { background: #223546; text-decoration: none; } -.btn.approve { background: var(--approve); border-color: var(--approve); } -.btn.deny { background: var(--deny); border-color: var(--deny); } -.btn.ghost { background: transparent; color: var(--ink); } -.btn.small, button.small { padding: .3rem .8rem; font-size: .82rem; margin-top: 0; } - -/* the signature: rubber-stamp status */ -.stamp { - display: inline-block; padding: .1rem .55rem; font-size: .72rem; font-weight: 600; - letter-spacing: .12em; text-transform: uppercase; border: 2px solid; - border-radius: 4px; transform: rotate(-2deg); -} -.stamp.approved { color: var(--approve); border-color: var(--approve); } -.stamp.denied { color: var(--deny); border-color: var(--deny); } -.stamp.pending { color: var(--pending); border-color: var(--pending); } -.stamp.big { font-size: 1.1rem; padding: .3rem 1rem; } - -.chip { - display: inline-block; font-size: .72rem; padding: .05rem .5rem; - border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); -} - -/* tables */ -table { width: 100%; border-collapse: collapse; font-size: .9rem; } -th { text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; - color: var(--ink-soft); padding: .5rem .6rem; border-bottom: 2px solid var(--line); } -td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -tr:hover td { background: var(--hover); } - -/* ledger (audit log) */ -.ledger { list-style: none; margin: 0; padding: 0; font-size: .85rem; } -.ledger li { - display: grid; grid-template-columns: 175px 170px 1fr; gap: 1rem; - padding: .55rem .2rem; border-bottom: 1px dashed var(--line); -} -.ledger .t { color: var(--ink-soft); } -.ledger .e { font-weight: 600; } -@media (max-width: 700px) { .ledger li { grid-template-columns: 1fr; gap: .1rem; } } - -/* stat row */ -.stats { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; } -.stat { flex: 1 1 120px; background: var(--card); border: 1px solid var(--line); - border-radius: var(--radius); padding: .8rem 1rem; } -.stat b { display: block; font-size: 1.5rem; font-family: "IBM Plex Mono", monospace; } -.stat span { font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft); } - -.flash { padding: .7rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: .92rem; } -.flash.ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-line); color: var(--flash-ok-ink); } -.flash.error { background: var(--flash-err-bg); border: 1px solid var(--flash-err-line); color: var(--flash-err-ink); } - -/* login */ -.login-box { max-width: 380px; margin: 12vh auto 0; } -.login-box .brand-lg { font-size: 1.7rem; font-weight: 700; } -.login-box .brand-lg span { color: var(--approve); } - -.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.5rem; } -@media (max-width: 700px) { .grid2 { grid-template-columns: 1fr; } } - -.rowline { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; } -form.inline { display: inline; } -details { margin-top: .4rem; } -summary { cursor: pointer; font-size: .85rem; color: var(--focus); } -.muted { color: var(--ink-soft); font-size: .85rem; } -.pre { white-space: pre-wrap; font-size: .9rem; background: var(--hover); border: 1px solid var(--line); - border-radius: 6px; padding: .8rem 1rem; } - -@media (prefers-reduced-motion: no-preference) { - .stamp.big { animation: stampIn .25s ease-out; } - @keyframes stampIn { from { transform: rotate(-2deg) scale(1.6); opacity: 0; } - to { transform: rotate(-2deg) scale(1); opacity: 1; } } -} - -/* footer */ -.footer { - max-width: 980px; margin: 0 auto; padding: 1.2rem 1.25rem 2rem; - font-size: .8rem; color: var(--ink-soft); text-align: center; - border-top: 1px solid var(--line); -} - -/* theme toggle */ -.theme-toggle { - margin: 0; padding: .15rem .55rem; font-size: 1rem; line-height: 1; - background: transparent; border: 1px solid #4a5b68; color: #b9c6cf; - border-radius: 999px; cursor: pointer; -} -.theme-toggle:hover { color: #fff; border-color: #7fd6c2; background: transparent; } -.theme-toggle.floating { - position: fixed; top: 1rem; right: 1rem; z-index: 10; - border-color: var(--line); color: var(--ink-soft); -} -.theme-toggle.floating:hover { color: var(--ink); border-color: var(--focus); } - -/* email image library */ -.imggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 1rem; } -.imgcard { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--field); } -.imgcard img { display: block; width: 100%; height: 110px; object-fit: contain; background: #fff; } -.imgmeta { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; } -.imgmeta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.imgmeta .tag { - background: var(--hover); border: 1px dashed var(--line); border-radius: 5px; - padding: .15rem .4rem; cursor: copy; word-break: break-all; white-space: normal; -} diff --git a/Approval Workflow/templates/account.html b/Approval Workflow/templates/account.html deleted file mode 100644 index 90eb590..0000000 --- a/Approval Workflow/templates/account.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} -{% block title %}Configure profile — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Configure profile

-

Signed in as {{ user.username }}

- -
-

Profile

-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Change password

-
- -
-
- - -
-
-
- - -
-
- - -
-
- -
-
- -
-

Two-factor authentication (MFA)

- - {% if user.mfa_secret %} -

enabled

-

Signing in requires your password and a 6-digit code from your authenticator app.

-
- - - - -
- - {% elif setup_secret %} -

1 — Scan this QR code with Google Authenticator, Authy, 1Password, or any TOTP app. - If you can't scan, enter the secret manually.

-
-

Secret: {{ setup_secret }}

-

2 — Enter the 6-digit code the app shows to confirm.

-
- - - -
-
- - -
- - - - {% else %} -

disabled

-

Add a second sign-in step: your password plus a 6-digit code from an authenticator app on your phone.

-
- - -
- {% endif %} -
-{% endblock %} diff --git a/Approval Workflow/templates/admin.html b/Approval Workflow/templates/admin.html deleted file mode 100644 index d42a305..0000000 --- a/Approval Workflow/templates/admin.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}Workflows & requests — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Workflows & requests

-

Create workflows, watch every request, and open any of them for the full audit trail.

- -
-
{{ stats.get('pending', 0) }}Pending
-
{{ stats.get('approved', 0) }}Approved
-
{{ stats.get('denied', 0) }}Denied
-
- -

Workflows

-
- - - {% for w in workflows %} - - - - - - - - {% else %} - - {% endfor %} -
NameApproval chainKnowledge chainStatus
{{ w.name }}{% if w.description %}
{{ w.description }}{% endif %}
{% for a in w.approvers | fromjson %}{{ loop.index }}. {{ a }}{% if not loop.last %}
{% endif %}{% endfor %}
{% for a in w.knowledge | fromjson %}{{ a }}{% if not loop.last %}
{% endif %}{% else %}{% endfor %}
{% if w.active %}active{% else %}paused{% endif %}Edit
No workflows yet — create the first one.
- New workflow -
- -

Requests

-
- - - {% for r in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
#WorkflowFromSubjectStatusReceived
{{ r.id }}{{ r.wf_name }}{{ r.requester }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
No requests yet.
-
-{% endblock %} diff --git a/Approval Workflow/templates/base.html b/Approval Workflow/templates/base.html deleted file mode 100644 index 26533d9..0000000 --- a/Approval Workflow/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - -{% block title %}MartinhalApprovalFlow{% endblock %} - - - - - - -{% if session.get('user_id') %} -
- MartinhalApprovalFlow - -
- - Configure profile - {{ session['username'] }} - Sign out -
-
-{% else %} - -{% endif %} -
- {% with msgs = get_flashed_messages(with_categories=true) %} - {% for cat, m in msgs %}
{{ m }}
{% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
© 2026 Martinhal IT - Joao Vaz - Version 9.3
- - diff --git a/Approval Workflow/templates/decision.html b/Approval Workflow/templates/decision.html deleted file mode 100644 index 4e54315..0000000 --- a/Approval Workflow/templates/decision.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} -{% block title %}Decision — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/templates/index.html b/Approval Workflow/templates/index.html deleted file mode 100644 index 00215d2..0000000 --- a/Approval Workflow/templates/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% block title %}New request — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Send a request for approval

-

Pick a workflow, write your message, and it goes straight to that workflow's approver.

- -{% if workflows %} -
-
- - - - - - - - - - - -
-
-{% else %} -
-

You don't have access to any workflow yet. Ask an administrator to grant you access.

-
-{% endif %} - -{% if my_requests %} -

Your recent requests

-
- - - {% for r in my_requests %} - - - - - - - - {% endfor %} -
#WorkflowSubjectStatusSent
{{ r.id }}{{ r.wf_name }}{{ r.subject }}{{ r.status }}{{ r.created_at }}
-
-{% endif %} -{% endblock %} diff --git a/Approval Workflow/templates/login.html b/Approval Workflow/templates/login.html deleted file mode 100644 index 6254420..0000000 --- a/Approval Workflow/templates/login.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in — MartinhalApprovalFlow{% endblock %} -{% block content %} - -{% endblock %} diff --git a/Approval Workflow/templates/mail_status.html b/Approval Workflow/templates/mail_status.html deleted file mode 100644 index 929da67..0000000 --- a/Approval Workflow/templates/mail_status.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mail status — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Mail status

-

Every email the app tried to send, with its delivery result and whether a copy was saved to the Sent folder.

- -{% if not configured %} -
Mail account not configured — MAIL_USER is empty, so messages are printed to the server console (dev mode) instead of being sent.
-{% endif %} - -
-
{{ stats.get('sent', 0) }}Sent
-
{{ stats.get('error', 0) }}Errors
-
{{ copy_fails }}Sent-copy failures
-
{{ stats.get('dev', 0) }}Dev mode
-
- -
- - - {% for m in rows %} - - - - - - - - - {% else %} - - {% endfor %} -
WhenToSubjectRequestDeliverySent folder
{{ m.at }}{{ m.recipients }}{{ m.subject }} - {% if m.detail %}
{{ m.detail }}{% endif %}
{% if m.request_id %}#{{ m.request_id }}{% else %}—{% endif %} - {% if m.status == 'sent' %}sent - {% elif m.status == 'error' %}error - {% else %}dev{% endif %} - - {% if m.sent_copy.startswith('saved') %}{{ m.sent_copy }} - {% elif m.sent_copy.startswith('failed') %}copy failed
{{ m.sent_copy }} - {% else %}{{ m.sent_copy }}{% endif %} -
No mail has been sent yet.
-
-{% endblock %} diff --git a/Approval Workflow/templates/request_detail.html b/Approval Workflow/templates/request_detail.html deleted file mode 100644 index b6e8c1c..0000000 --- a/Approval Workflow/templates/request_detail.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "base.html" %} -{% block title %}Request #{{ req.id }} — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All requests

-
-

Request #{{ req.id }}

- {{ req.status }} -
-

Workflow {{ req.wf_name }} · received {{ req.created_at }}

- -
-

From: {{ req.requester }}
- Subject: {{ req.subject }}

-
{{ req.body }}
- {% if attachments %} -

Attachments (included in every approval mail):
- {% for att in attachments %} - {{ att.name }}{% if not loop.last %} · {% endif %} - {% endfor %} -

- {% endif %} - {% if req.decided_by %} -

Final decision by {{ req.decided_by }} at {{ req.decided_at }}

- {% endif %} - {% if req.status == 'denied' and req.deny_reason %} -

Reason for denial:

-
{{ req.deny_reason }}
- {% endif %} - {% if req.status == 'pending' %} -
-
- - Same effect as the current approver's email links. -
-
-
- - - -
- {% endif %} -
- -

Approval chain

-
-
    - {% for a in chain %} -
  • - step {{ loop.index }} / {{ chain | length }} - {{ a }} - - {% if req.status == 'denied' and loop.index0 == req.current_step %}denied here - {% elif loop.index0 < req.current_step or req.status == 'approved' %}approved - {% elif loop.index0 == req.current_step and req.status == 'pending' %}waiting - {% elif req.status == 'denied' %}never reached — workflow stopped - {% else %}not yet reached{% endif %} - -
  • - {% endfor %} -
-
- -

People involved so far

-
- {% for p in participants %}{{ p }} {% else %}None recorded.{% endfor %} -
- -

Full log

-
-
    - {% for l in logs %} -
  • {{ l.at }}{{ l.event }}{{ l.detail }}
  • - {% endfor %} -
-
- - -
-
-{% endblock %} diff --git a/Approval Workflow/templates/users.html b/Approval Workflow/templates/users.html deleted file mode 100644 index 3f658f9..0000000 --- a/Approval Workflow/templates/users.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "base.html" %} -{% block title %}Users — MartinhalApprovalFlow{% endblock %} -{% block content %} -

Users

-

Create accounts and choose which workflows each user can send requests to. Admins can use every workflow. Users fill in their own details (name, unit, e-mail, MFA) on their Configure profile page; you can disable MFA here if someone loses their device.

- -
-

Add a user

-
- -
-
-
-
-
-
- - -
-
- -{% for u in users %} -
-
- {{ u.username }} - {% if u.first_name or u.last_name %}{{ u.first_name }} {{ u.last_name }}{% endif %} - {% if u.unit_location %}{{ u.unit_location }}{% endif %} - {% if u.is_admin %}admin{% endif %} - {% if u.mfa_secret %}MFA on{% endif %} - {% if u.email %}{{ u.email }}{% endif %} - - {% if u.mfa_secret %} -
- - - -
- {% endif %} - {% if u.id != session['user_id'] %} -
- - - -
- {% endif %} -
- - {% if not u.is_admin %} -
- - - - {% for w in workflows %} - - {% else %} - No workflows exist yet. - {% endfor %} - {% if workflows %}{% endif %} -
- {% endif %} - -
- Reset password -
- - - - -
-
-
-{% endfor %} -{% endblock %} diff --git a/Approval Workflow/templates/workflow_edit.html b/Approval Workflow/templates/workflow_edit.html deleted file mode 100644 index 47202ae..0000000 --- a/Approval Workflow/templates/workflow_edit.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ 'Edit' if wf else 'New' }} workflow — MartinhalApprovalFlow{% endblock %} -{% block content %} -

← All workflows

-

{{ 'Edit workflow' if wf else 'New workflow' }}

-

Each workflow has its own allowed sender domain, its own approver, and its own email wording.

- -
-
-

Basics

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
- -
-

Email wording

-

Placeholders: {workflow} {requester} {subject} {body} {request_id} {step} {total_steps} {decided_by} {created_at} {summary} {deny_reason} {approve_url} {deny_url}. Leave a field empty to use the default.

- - - - - - -
-
- - - - -
-
- - - - -
-
-
- - -
- -
-

Images for email bodies

-

These images can be used in the approval request body, approved notice body and - denied notice body above. Copy an image's tag into the text where the image should appear — - it is embedded in the email at that spot. PNG, JPG or GIF, up to 2 MB each. The library is shared by all workflows.

- - {% if images %} -
- {% for name in images %} -
- {{ name }} -
- {{ name }} - {{ '{image:' + name + '}' }} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

No images uploaded yet.

- {% endif %} - -
- - - -
-
-{% endblock %} diff --git a/Approval Workflow/totp.py b/Approval Workflow/totp.py deleted file mode 100644 index 909a241..0000000 --- a/Approval Workflow/totp.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard -library. Compatible with Google Authenticator, Authy, 1Password, etc.""" -import base64 -import hashlib -import hmac -import secrets -import struct -import time -from urllib.parse import quote - -ISSUER = "MartinhalApprovalFlow" - - -def new_secret() -> str: - """A new base32 secret to enroll in an authenticator app.""" - return base64.b32encode(secrets.token_bytes(20)).decode() - - -def _code_at(secret: str, counter: int) -> str: - key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True) - digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest() - offset = digest[-1] & 15 - number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000 - return f"{number:06d}" - - -def verify(secret: str, code: str) -> bool: - """Check a 6-digit code, allowing one 30s step of clock drift each way.""" - code = (code or "").strip().replace(" ", "") - if not (secret and code.isdigit() and len(code) == 6): - return False - counter = int(time.time() // 30) - return any(hmac.compare_digest(_code_at(secret, counter + drift), code) - for drift in (-1, 0, 1)) - - -def otpauth_uri(username: str, secret: str) -> str: - """URI encoded as a QR code for authenticator apps.""" - return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}" - f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30") diff --git a/Barcode/.dockerignore b/Barcode/.dockerignore deleted file mode 100644 index ec790d1..0000000 --- a/Barcode/.dockerignore +++ /dev/null @@ -1,38 +0,0 @@ -# Version control -.git -.gitignore -.gitattributes - -# Documentation that doesn't belong in the image -README.md -LICENSE -CHANGELOG.md -docs/ - -# Editor / OS junk -.vscode -.idea -.DS_Store -Thumbs.db -*.swp -*.swo -*~ - -# Local env files (use .env at the compose level, not inside the image) -.env -.env.local -.env.*.local - -# Build artefacts that shouldn't be shipped -node_modules -dist -build -*.log -npm-debug.log* - -# CI -.github/ - -# Test outputs -coverage/ -.nyc_output/ diff --git a/Barcode/.env.example b/Barcode/.env.example deleted file mode 100644 index c86015c..0000000 --- a/Barcode/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Copy to `.env` and tweak as needed. -# These values feed docker-compose.yml. - -# Public port on the host machine. Defaults to 8080. -HOST_PORT=8080 diff --git a/Barcode/Dockerfile b/Barcode/Dockerfile deleted file mode 100644 index f6561ae..0000000 --- a/Barcode/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# ============================================================================= -# Cipher Barcode Studio — production image -# Base: nginx 1.30.1 on Alpine 3.23 (unprivileged variant — runs as UID 101) -# ============================================================================= - -FROM nginxinc/nginx-unprivileged:1.30.1-alpine3.23-slim - -# OCI labels for traceability -LABEL org.opencontainers.image.title="Cipher Barcode Studio" \ - org.opencontainers.image.description="Static web app for generating barcodes in 22 symbologies and exporting JPG/PNG" \ - org.opencontainers.image.version="1.0.0" \ - org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.source="https://example.com/cipher-barcode-studio" - -# The unprivileged image runs as user 101 (nginx). All file ops use --chown. -USER root - -# Apply the latest Alpine security patches at build time, then strip the cache -RUN apk update && apk upgrade --no-cache && rm -rf /var/cache/apk/* - -# Replace the default nginx config with our hardened one -COPY --chown=nginx:nginx nginx/nginx.conf /etc/nginx/nginx.conf - -# Copy the static site -COPY --chown=nginx:nginx web/ /usr/share/nginx/html/ - -# Make sure runtime dirs the unprivileged user needs are writable -RUN mkdir -p /tmp/client_body /tmp/proxy /tmp/fastcgi /tmp/uwsgi /tmp/scgi \ - && chown -R nginx:nginx /tmp/client_body /tmp/proxy /tmp/fastcgi /tmp/uwsgi /tmp/scgi /var/log/nginx \ - && chmod -R 755 /usr/share/nginx/html - -# Drop back to the non-root user for runtime -USER nginx - -# Internal port — unprivileged nginx listens on 8080 -EXPOSE 8080 - -# Healthcheck driven by /healthz from nginx.conf -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --quiet --tries=1 --spider http://127.0.0.1:8080/healthz || exit 1 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/Barcode/LICENSE b/Barcode/LICENSE deleted file mode 100644 index 8d15c60..0000000 --- a/Barcode/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -MIT License - -Copyright (c) 2026 Cipher Barcode Studio - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -Third-party software shipped with this project retains its own license: - -* JsBarcode (MIT) — https://github.com/lindell/JsBarcode -* bwip-js (MIT) — https://github.com/metafloor/bwip-js -* nginx (BSD-2-Clause) — https://nginx.org/LICENSE diff --git a/Barcode/OLD_VERSIONS/Barcode.zip b/Barcode/OLD_VERSIONS/Barcode.zip deleted file mode 100644 index 8c791b3..0000000 Binary files a/Barcode/OLD_VERSIONS/Barcode.zip and /dev/null differ diff --git a/Barcode/OLD_VERSIONS/BarcodeStudio v1.0.zip b/Barcode/OLD_VERSIONS/BarcodeStudio v1.0.zip deleted file mode 100644 index 3fd4b24..0000000 Binary files a/Barcode/OLD_VERSIONS/BarcodeStudio v1.0.zip and /dev/null differ diff --git a/Barcode/README.md b/Barcode/README.md deleted file mode 100644 index 6cedcdd..0000000 --- a/Barcode/README.md +++ /dev/null @@ -1,385 +0,0 @@ -# Cipher · Barcode Studio - -A self-contained, browser-based barcode generator that converts text or numeric sequences into 22 different barcode symbologies and exports them as downloadable JPG (or PNG) images. Designed to run as a single hardened Docker container with no backend, no database, and no telemetry — everything happens in the user's browser. - -![status](https://img.shields.io/badge/status-stable-success) ![docker](https://img.shields.io/badge/docker-compose-blue) ![license](https://img.shields.io/badge/license-MIT-lightgrey) - ---- - -## Table of Contents - -1. [Features](#features) -2. [Supported Barcode Formats](#supported-barcode-formats) -3. [Architecture](#architecture) -4. [Requirements](#requirements) -5. [Quick Start](#quick-start) -6. [Configuration](#configuration) -7. [Project Structure](#project-structure) -8. [Security](#security) -9. [Dependency Versions & Vulnerability Status](#dependency-versions--vulnerability-status) -10. [Operations](#operations) -11. [Updating & Maintenance](#updating--maintenance) -12. [Troubleshooting](#troubleshooting) -13. [Development](#development) -14. [License](#license) - ---- - -## Features - -* **22 barcode symbologies** spanning retail/GS1, linear/industrial, pharma, postal and 2D matrix codes. -* **Live example preview** — every symbology shows a sample render the moment it's selected. -* **Batch generation** — paste lines into the input area and each line becomes its own barcode card. -* **Per-card text toggle** — show or hide the human-readable caption beneath each barcode, then download it in that state. -* **JPG and PNG export** — individual downloads per barcode plus a one-click "Download All as JPG" for the whole batch. -* **Adjustable scale and JPG quality** for print-ready output. -* **Fully offline-capable** once loaded — every library is vendored locally, no third-party CDN at runtime (web fonts are the only external request and are optional). -* **No backend** — runs entirely as static assets served by nginx. No database, no API, no user data leaves the browser. - ---- - -## Supported Barcode Formats - -| Group | Symbology | Notes | -|---|---|---| -| Retail · GS1 | EAN-13, EAN-8, UPC-A, UPC-E, ISBN-13, GS1 DataBar, GS1-128 | ISBN-10 auto-converts to ISBN-13 | -| Linear · Industrial | Code-128, Code-39, Code-39 Full ASCII, Code-93, Code-11, Code 2of5 Interleaved, MSI Plessey, Flattermarken, Telepen Alpha | | -| Pharma · Specialty | Pharmacode One-Track, Pharmacode Two-Track | Laetus binary codes | -| Postal | KIX | Dutch PostNL routing | -| 2D · Matrix | QR Code, Data Matrix, PDF417 | | - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Browser │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ index.html (UI + state) │ │ -│ │ ├── JsBarcode (linear codes) │ │ -│ │ └── bwip-js (specialty + 2D codes) │ │ -│ │ ─→ HTMLCanvasElement → JPG / PNG Blob │ │ -│ └───────────────────────────────────────────────────┘ │ -└──────────────────────────────┬──────────────────────────────┘ - │ HTTP (static assets only) - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Docker container · nginx 1.30.1 · alpine 3.23 │ -│ • runs as non-root (UID 101) │ -│ • read-only root filesystem │ -│ • all caps dropped, no-new-privileges │ -│ • CSP / HSTS / nosniff / X-Frame-Options / COOP / CORP │ -└─────────────────────────────────────────────────────────────┘ -``` - -Encoding happens **entirely in the browser** — the container only ships static HTML/JS. The image cannot leak data because there is no application server to leak through. - ---- - -## Requirements - -| Component | Minimum version | Why | -|---|---|---| -| Docker Engine | **24.0+** | Buildkit, compose-v2 features | -| Docker Compose | **v2.20+** | Modern compose schema (`deploy.resources`, `tmpfs` options) | -| Host OS | Linux / macOS / Windows (WSL2) | Anything Docker supports | -| RAM | 64 MB free | Container reserves 32 MB, caps at 128 MB | -| Disk | ~50 MB | Image is ~40 MB | -| Open port | one host port (default 8080) | Reconfigurable via `.env` | - -Verify your toolchain: - -```bash -docker --version # → Docker version 24.x.x or newer -docker compose version # → Docker Compose version v2.20+ or newer -``` - -If you're on an older Docker that only has the standalone `docker-compose` binary, the file still works — just replace `docker compose` with `docker-compose` in the commands below. - ---- - -## Quick Start - -```bash -# 1. Unzip and enter the project -unzip cipher-barcode-studio.zip -cd cipher-barcode-studio - -# 2. (Optional) override the host port -cp .env.example .env # then edit HOST_PORT if needed - -# 3. Build and start -docker compose up -d --build - -# 4. Open in browser -# http://localhost:8080 -``` - -That's it. To stop: - -```bash -docker compose down -``` - -To stop and remove the built image as well: - -```bash -docker compose down --rmi local -``` - ---- - -## Configuration - -All runtime configuration lives in `.env` (copied from `.env.example`): - -| Variable | Default | Description | -|---|---|---| -| `HOST_PORT` | `8080` | Port published on the host machine | - -The container itself always listens on `8080` internally — only the host-side port is configurable. - -### Putting it behind a reverse proxy - -If you're running this behind Traefik, Caddy, nginx, or similar: - -```yaml -# example with Traefik labels -services: - cipher: - # ...existing config... - labels: - - "traefik.enable=true" - - "traefik.http.routers.cipher.rule=Host(`barcode.example.com`)" - - "traefik.http.routers.cipher.tls=true" - - "traefik.http.routers.cipher.tls.certresolver=letsencrypt" - - "traefik.http.services.cipher.loadbalancer.server.port=8080" -``` - -When fronted by HTTPS, the `Strict-Transport-Security` header in `nginx.conf` will kick in automatically. - ---- - -## Project Structure - -``` -cipher-barcode-studio/ -├── docker-compose.yml # Orchestration + runtime hardening -├── Dockerfile # Image build steps -├── .dockerignore # Keep the build context lean -├── .env.example # Template for runtime config -├── LICENSE # MIT + third-party attributions -├── README.md # You are here -├── nginx/ -│ └── nginx.conf # Hardened nginx config w/ CSP & security headers -└── web/ - ├── index.html # The full application (single HTML file) - └── vendor/ - ├── JsBarcode.all.min.js - └── bwip-js-min.js -``` - ---- - -## Security - -This image is built with a defense-in-depth posture even though it serves static content with no user data: - -### Container hardening - -| Control | Mechanism | -|---|---| -| Non-root execution | Image's `USER` is `nginx` (UID 101). Compose pins `user: "101:101"`. | -| Read-only rootfs | `read_only: true` in compose; writable paths exposed via `tmpfs`. | -| Capability drop | `cap_drop: [ALL]` — no Linux capabilities granted. | -| No privilege escalation | `security_opt: no-new-privileges:true`. | -| Resource limits | CPU capped at 0.5 cores, memory at 128 MB. | -| Minimal base | `nginxinc/nginx-unprivileged:alpine3.23-slim` — Alpine, no extra packages, security upgrades applied at build. | -| Restricted log size | json-file driver capped at 5 MB × 3 rotations. | - -### HTTP hardening (in `nginx/nginx.conf`) - -| Header | Purpose | -|---|---| -| `Content-Security-Policy` | Restricts scripts/styles/fonts/images to `self` + Google Fonts only | -| `Strict-Transport-Security` | One-year HSTS with subdomain coverage (effective behind TLS) | -| `X-Frame-Options: DENY` + CSP `frame-ancestors 'none'` | Clickjacking protection | -| `X-Content-Type-Options: nosniff` | Disables MIME sniffing | -| `Referrer-Policy: strict-origin-when-cross-origin` | Limits referrer leakage | -| `Permissions-Policy` | Disables camera, mic, geolocation, USB, payment APIs | -| `Cross-Origin-Opener-Policy: same-origin` | Window isolation | -| `Cross-Origin-Resource-Policy: same-origin` | Resource isolation | -| `server_tokens off` | Hides nginx version | - -Also: request size capped at 1 KB (this is a static GET-only service), hidden files (`/\.`) blocked, `/healthz` lightweight endpoint for orchestrators. - -### Supply chain - -* JavaScript libraries are **vendored** under `web/vendor/`, not loaded from CDN at runtime. -* Each ` - - - - - - -
-
-
Barcode Studio · v1.1
-
J.Vaz 2026
-
-
- System Live - Formats 22 - Output JPG / PNG -
-
- -
-

Encode anything.
Print everything.

-
- Studio Manual - A precision instrument for converting text and numbers into industrial-grade barcodes. - Select a symbology, paste your data line-by-line, and export downloadable JPG images — - one per line, or all at once. -
-
- -
- - - - - -
- - -
-
-

-
Select a format
-

Pick a symbology from the panel on the left to begin.

-
-
-
-
Loading…
-
-
- - -
-
-

Data Input

-
One barcode per line · to add another
-
- - -
- - - -
- -
- - - -
-
- - -
-
-

Output

-
Generated · 0
-
-
-
No barcodes generated yet
-
-
- -
-
- -
-
Barcode Studio
-
-
JPG · PNG · Print-Ready
-
- -
Saved
- - - - - diff --git a/Barcode/web/vendor/JsBarcode.all.min.js b/Barcode/web/vendor/JsBarcode.all.min.js deleted file mode 100644 index 80d1092..0000000 --- a/Barcode/web/vendor/JsBarcode.all.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! JsBarcode v3.12.3 | (c) Johan Lindell | MIT license */ -!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=16)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.default=function t(e,n){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.data=e,this.text=n.text||e,this.options=n}},function(t,e,n){"use strict";var r;function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}Object.defineProperty(e,"__esModule",{value:!0});var i=e.SET_A=0,a=e.SET_B=1,u=e.SET_C=2,f=(e.SHIFT=98,e.START_A=103),c=e.START_B=104,s=e.START_C=105;e.MODULO=103,e.STOP=106,e.FNC1=207,e.SET_BY_CODE=(o(r={},f,i),o(r,c,a),o(r,s,u),r),e.SWAP={101:i,100:a,99:u},e.A_START_CHAR=String.fromCharCode(208),e.B_START_CHAR=String.fromCharCode(209),e.C_START_CHAR=String.fromCharCode(210),e.A_CHARS="[\0-_È-Ï]",e.B_CHARS="[ -È-Ï]",e.C_CHARS="(Ï*[0-9]{2}Ï*)",e.BARS=[11011001100,11001101100,11001100110,10010011e3,10010001100,10001001100,10011001e3,10011000100,10001100100,11001001e3,11001000100,11000100100,10110011100,10011011100,10011001110,10111001100,10011101100,10011100110,11001110010,11001011100,11001001110,11011100100,11001110100,11101101110,11101001100,11100101100,11100100110,11101100100,11100110100,11100110010,11011011e3,11011000110,11000110110,10100011e3,10001011e3,10001000110,10110001e3,10001101e3,10001100010,11010001e3,11000101e3,11000100010,10110111e3,10110001110,10001101110,10111011e3,10111000110,10001110110,11101110110,11010001110,11000101110,11011101e3,11011100010,11011101110,11101011e3,11101000110,11100010110,11101101e3,11101100010,11100011010,11101111010,11001000010,11110001010,1010011e4,10100001100,1001011e4,10010000110,10000101100,10000100110,1011001e4,10110000100,1001101e4,10011000010,10000110100,10000110010,11000010010,1100101e4,11110111010,11000010100,10001111010,10100111100,10010111100,10010011110,10111100100,10011110100,10011110010,11110100100,11110010100,11110010010,11011011110,11011110110,11110110110,10101111e3,10100011110,10001011110,10111101e3,10111100010,11110101e3,11110100010,10111011110,10111101110,11101011110,11110101110,11010000100,1101001e4,11010011100,1100011101011]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.SIDE_BIN="101",e.MIDDLE_BIN="01010",e.BINARIES={L:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],G:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"],R:["1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"],O:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],E:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"]},e.EAN2_STRUCTURE=["LL","LG","GL","GG"],e.EAN5_STRUCTURE=["GGLLL","GLGLL","GLLGL","GLLLG","LGGLL","LLGGL","LLLGG","LGLGL","LGLLG","LLGLG"],e.EAN13_STRUCTURE=["LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG","LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e.default=function(t,e,n){var o=t.split("").map((function(t,n){return r.BINARIES[e[n]]})).map((function(e,n){return e?e[t[n]]:""}));if(n){var i=t.length-1;o=o.map((function(t,e){return e=200){i=t.shift()-105;var a=u.SWAP[i];void 0!==a?o=e.next(t,n+1,a):(r!==u.SET_A&&r!==u.SET_B||i!==u.SHIFT||(t[0]=r===u.SET_A?t[0]>95?t[0]-96:t[0]:t[0]<32?t[0]+96:t[0]),o=e.next(t,n+1,r))}else i=e.correctIndex(t,r),o=e.next(t,n+1,r);var f=i*n;return{result:e.getBar(i)+o.result,checksum:f+o.checksum}}}]),e}(a.default);e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.mod10=function(t){for(var e=0,n=0;n10*n.width?10*n.width:n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"encode",value:function(){return this.options.flat?this.encodeFlat():this.encodeGuarded()}},{key:"leftText",value:function(t,e){return this.text.substr(t,e)}},{key:"leftEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"rightText",value:function(t,e){return this.text.substr(t,e)}},{key:"rightEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"encodeGuarded",value:function(){var t={fontSize:this.fontSize},e={height:this.guardHeight};return[{data:o.SIDE_BIN,options:e},{data:this.leftEncode(),text:this.leftText(),options:t},{data:o.MIDDLE_BIN,options:e},{data:this.rightEncode(),text:this.rightText(),options:t},{data:o.SIDE_BIN,options:e}]}},{key:"encodeFlat",value:function(){return{data:[o.SIDE_BIN,this.leftEncode(),o.MIDDLE_BIN,this.rightEncode(),o.SIDE_BIN].join(""),text:this.text}}}]),e}(a(n(0)).default);e.default=u},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return-1!==this.data.search(/^[0-9]{12}$/)&&this.data[11]==u(this.data)}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=(0,o.default)(this.data.substr(0,6),"LLLLLL"),t+="01010",t+=(0,o.default)(this.data.substr(6,6),"RRRRRR"),{data:t+="101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text.substr(0,1),options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101"+(0,o.default)(this.data[0],"L"),options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(1,5),"LLLLL"),text:this.text.substr(1,5),options:{fontSize:this.fontSize}}),t.push({data:"01010",options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(6,5),"RRRRR"),text:this.text.substr(6,5),options:{fontSize:this.fontSize}}),t.push({data:(0,o.default)(this.data[11],"R")+"101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text.substr(11,1),options:{textAlign:"right",fontSize:this.fontSize}}),t}}]),e}(i(n(0)).default);function u(t){var e,n=0;for(e=1;e<11;e+=2)n+=parseInt(t[e]);for(e=0;e<11;e+=2)n+=3*parseInt(t[e]);return(10-n%10)%10}e.default=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?e.fontSize+e.textMargin:0)+e.marginTop+e.marginBottom}function u(t,e,n){if(n.displayValue&&ee&&(e=t[n].height);return e},e.getEncodingHeight=a,e.getBarcodePadding=u,e.calculateEncodingAttributes=function(t,e,n){for(var r=0;r=i(t);return e+String.fromCharCode(r?206:205)+u(t,r)}e.default=function(t){var e=void 0;if(a(t).length>=2)e=r.C_START_CHAR+f(t);else{var n=o(t)>i(t);e=(n?r.A_START_CHAR:r.B_START_CHAR)+u(t,n)}return e.replace(/[\xCD\xCE]([^])[\xCD\xCE]/,(function(t,e){return String.fromCharCode(203)+e}))}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return this.isValid}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=this.encodeMiddleDigits(),{data:t+="010101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text[0],options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101",options:{height:this.guardHeight}}),t.push({data:this.encodeMiddleDigits(),text:this.text.substring(1,7),options:{fontSize:this.fontSize}}),t.push({data:"010101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text[7],options:{textAlign:"right",fontSize:this.fontSize}}),t}},{key:"encodeMiddleDigits",value:function(){var t=this.upcA[0],e=this.upcA[this.upcA.length-1],n=s[parseInt(e)][parseInt(t)];return(0,o.default)(this.middleDigits,n)}}]),e}(i.default);function p(t,e){for(var n=parseInt(t[t.length-1]),r=c[n],o="",i=0,u=0;u=3&&this.number<=131070}}]),e}(((r=i)&&r.__esModule?r:{default:r}).default);e.pharmacode=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.codabar=void 0;var r,o=function(){function t(t,e){for(var n=0;n":["(%)","I"],"?":["(%)","J"],"@":["(%)","V"],"[":["(%)","K"],"\\":["(%)","L"],"]":["(%)","M"],"^":["(%)","N"],_:["(%)","O"],"`":["(%)","W"],a:["(+)","A"],b:["(+)","B"],c:["(+)","C"],d:["(+)","D"],e:["(+)","E"],f:["(+)","F"],g:["(+)","G"],h:["(+)","H"],i:["(+)","I"],j:["(+)","J"],k:["(+)","K"],l:["(+)","L"],m:["(+)","M"],n:["(+)","N"],o:["(+)","O"],p:["(+)","P"],q:["(+)","Q"],r:["(+)","R"],s:["(+)","S"],t:["(+)","T"],u:["(+)","U"],v:["(+)","V"],w:["(+)","W"],x:["(+)","X"],y:["(+)","Y"],z:["(+)","Z"],"{":["(%)","P"],"|":["(%)","Q"],"}":["(%)","R"],"~":["(%)","S"],"":["(%)","T"]}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?(n=0,o.textAlign="left"):"right"==t.textAlign?(n=e.width-1,o.textAlign="right"):(n=e.width/2,o.textAlign="center"),o.fillText(e.text,n,r))}},{key:"moveCanvasDrawing",value:function(t){this.canvas.getContext("2d").translate(t.width,0)}},{key:"restoreCanvas",value:function(){this.canvas.getContext("2d").restore()}}]),t}();e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0&&(this.drawRect(a-e.width*i,r,e.width*i,e.height,t),i=0);i>0&&this.drawRect(a-e.width*(i-1),r,e.width*i,e.height,t)}},{key:"drawSVGText",value:function(t,e,n){var r,o,i=this.document.createElementNS(f,"text");e.displayValue&&(i.setAttribute("font-family",e.font),i.setAttribute("font-size",e.fontSize),e.fontOptions.includes("bold")&&i.setAttribute("font-weight","bold"),e.fontOptions.includes("italic")&&i.setAttribute("font-style","italic"),o="top"==e.textPosition?e.fontSize-e.textMargin:e.height+e.textMargin+e.fontSize,"left"==e.textAlign||n.barcodePadding>0?(r=0,i.setAttribute("text-anchor","start")):"right"==e.textAlign?(r=n.width-1,i.setAttribute("text-anchor","end")):(r=n.width/2,i.setAttribute("text-anchor","middle")),i.setAttribute("x",r),i.setAttribute("y",o),i.appendChild(this.document.createTextNode(n.text)),t.appendChild(i))}},{key:"setSvgAttributes",value:function(t,e){var n=this.svg;n.setAttribute("width",t+"px"),n.setAttribute("height",e+"px"),n.setAttribute("x","0px"),n.setAttribute("y","0px"),n.setAttribute("viewBox","0 0 "+t+" "+e),n.setAttribute("xmlns",f),n.setAttribute("version","1.1")}},{key:"createGroup",value:function(t,e,n){var r=this.document.createElementNS(f,"g");return r.setAttribute("transform","translate("+t+", "+e+")"),n.appendChild(r),r}},{key:"setGroupOptions",value:function(t,e){t.setAttribute("fill",e.lineColor)}},{key:"drawRect",value:function(t,e,n,r,o){var i=this.document.createElementNS(f,"rect");return i.setAttribute("x",t),i.setAttribute("y",e),i.setAttribute("width",n),i.setAttribute("height",r),o.appendChild(i),i}}]),t}();e.default=c},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n=0&&$k[i]!==Infinity;i--);if(i<0){throw new Error("array-marker-not-found")}a=$k.splice(i+1,$j-1-i);$j=i}else if(!(a instanceof Array)){a=new Array(+arguments[0]);for(var i=0,l=a.length;i=0&&$k[mark]!==Infinity;mark-=2){if($k[mark-1]===Infinity){throw new Error("dict-malformed-stack")}}if(mark<0){throw new Error("dict-marker-not-found")}var d=new Map;for(var i=mark+1;i<$j;i+=2){var k=$k[i];var t=typeof k;if(t=="number"||t=="string"){d.set(k,$k[i+1])}else if(k instanceof Uint8Array){d.set($z(k),$k[i+1])}else{throw new Error("dict-not-a-valid-key("+k+")")}}$j=mark;return d}function $s(v){var t=typeof v;if(t==="number"){return new Uint8Array(v)}if(t!=="string"){v=""+v}var s=new Uint8Array(v.length);for(var i=0,l=v.length;i=0;i--){d[o+i]=s[i]}}}else if(d instanceof Array){var darr=d.b;var doff=o+d.o;var sarr=s.b;var soff=s.o;for(var i=0,l=s.length;i0&&$k[--$j]!==Infinity);}function $m(){for(var i=$j-1;i>=0&&$k[i]!==Infinity;i--);return $j-i-1}function $q(a){for(var i=0,l=a.length,b=a.b,o=a.o;ib}function $ge(a,b){if(a instanceof Uint8Array){a=$z(a)}if(b instanceof Uint8Array){b=$z(b)}return a>=b}function $an(a,b){return typeof a==="boolean"?a&&b:a&b}function $or(a,b){return typeof a==="boolean"?a||b:a|b}function $xo(a,b){return typeof a==="boolean"?a!=b:a^b}function $nt(a){return typeof a=="boolean"?!a:~a}function $bs(v,n){return n<0?$flr(v/$pow(2,-n)):v*$pow(2,n)}var $f=function(fa){return v=>{return(v|0)==v?v:(fa[0]=v,fa[0])}}(new Float32Array(1));function bwipp_raiseerror(){var info=$k[--$j];var name=$k[--$j];bwipp_error.set("errorname",name);bwipp_error.set("errorinfo",info);if(typeof info=="string"||info instanceof Uint8Array){throw new Error($z(name)+": "+$z(info))}else{throw $z(name)}}const _textOptions=new Map([["",""],["subspace",""],["split",""],["linegaps",1.2],["color","unset"],["xalign","left"],["yalign","above"],["direction","forward"],["font","OCR-B"],["size",10],["xoffset",0],["yoffset",0],["gaps",0]]);function bwipp_inittextoptions(){for(var grp=1;grp<=9;grp++){var map=_textOptions;for(var keys=map.keys(),i=0,l=map.size;i=3){var _k=$k[$j-1];var _l=$G(_k,0,3);var _m=$_.ctrl;var _n=$has(_m,_l);$k[$j++]=_m;$k[$j++]=_l;if(_n){$_.j=$_.j-1;var _p=$k[--$j];var _q=$k[--$j];$p($_.msg,$_.j,$g(_q,_p));$_.j=$_.j+1;var _v=$k[--$j];$k[$j++]=$G(_v,3,_v.length-3);break}else{$j-=2}}var _x=$k[$j-1];if(_x.length>=2){var _y=$k[$j-1];var _z=$G(_y,0,2);var _10=$_.ctrl;var _11=$has(_10,_z);$k[$j++]=_10;$k[$j++]=_z;if(_11){$_.j=$_.j-1;var _13=$k[--$j];var _14=$k[--$j];$p($_.msg,$_.j,$g(_14,_13));$_.j=$_.j+1;var _19=$k[--$j];$k[$j++]=$G(_19,2,_19.length-2);break}else{$j-=2}}var _1B=$k[$j-1];if(_1B.length>=3){var _1C=$k[$j-1];var _1D=$G(_1C,0,3);$k[$j++]=true;for(var _1E=0,_1F=_1D.length;_1E<_1F;_1E++){var _1G=$g(_1D,_1E);if(_1G<48||_1G>57){$k[$j-1]=false}}var _1H=$k[--$j];if(_1H){var _1I=$k[$j-1];var _1K=$i($G(_1I,0,3));$k[$j++]=_1K;if(_1K>255){$j-=2;$k[$j++]="bwipp.invalidOrdinal#492";$k[$j++]="Ordinal must be 000 to 255";bwipp_raiseerror()}$_.j=$_.j-1;var _1N=$k[--$j];$p($_.msg,$_.j,_1N);$_.j=$_.j+1;var _1Q=$k[--$j];$k[$j++]=$G(_1Q,3,_1Q.length-3)}}}if($_.parseonly||!$_.parsefnc||$g($_.msg,$_.j-1)!=94){break}$_.j=$_.j-1;var _1Y=$k[$j-1];if(_1Y.length<1){$k[$j-1]="bwipp.truncatedCaret#511";$k[$j++]="Caret character truncated";bwipp_raiseerror()}var _1Z=$k[$j-1];if($g(_1Z,0)==94){$p($_.msg,$_.j,94);$_.j=$_.j+1;var _1e=$k[--$j];$k[$j++]=$G(_1e,1,_1e.length-1);break}var _1g=$k[$j-1];if(_1g.length<3){$k[$j-1]="bwipp.truncatedFNC#521";$k[$j++]="Function character truncated";bwipp_raiseerror()}var _1h=$k[$j-1];if($eq($G(_1h,0,3),"ECI")&&$_.eci){var _1k=$k[$j-1];if(_1k.length<9){$k[$j-1]="bwipp.truncatedECI#526";$k[$j++]="ECI truncated";bwipp_raiseerror()}var _1l=$k[$j-1];var _1m=$G(_1l,3,6);$k[$j++]=_1m;for(var _1n=0,_1o=_1m.length;_1n<_1o;_1n++){var _1p=$g(_1m,_1n);if(_1p<48||_1p>57){$j-=2;$k[$j++]="bwipp.invalidECI#532";$k[$j++]="ECI must be 000000 to 999999";bwipp_raiseerror()}}var _1q=$k[--$j];var _1t=0;$F(_1q,function(){var _1r=$k[--$j];_1t=$f(_1t-(_1r-48))*10});$p($_.msg,$_.j,~~(_1t/10)-1e6);$_.j=$_.j+1;var _1x=$k[--$j];$k[$j++]=$G(_1x,9,_1x.length-9);break}var _1z=$k[$j-1];if(_1z.length<4){$k[$j-1]="bwipp.truncatedFNC#543";$k[$j++]="Function character truncated";bwipp_raiseerror()}var _20=$k[$j-1];var _21=$G(_20,0,4);var _23=$has($_.fncvals,_21);$k[$j++]=_21;if(!_23){var _24=$k[--$j];var _25=$s(_24.length+28);$P(_25,28,_24);$P(_25,0,"Unknown function character: ");$k[$j-1]="bwipp.unknownFNC#549";$k[$j++]=_25;bwipp_raiseerror()}var _29=$k[--$j];$p($_.msg,$_.j,$g($_.fncvals,_29));$_.j=$_.j+1;var _2E=$k[--$j];$k[$j++]=$G(_2E,4,_2E.length-4);break}}else{break}}if(!$_.parseonly){$k[$j++]=$G($_.msg,0,$_.j)}else{$k[$j++]=$s($_.j);for(var _2O=0,_2N=$_.j-1;_2O<=_2N;_2O+=1){var _2P=$k[$j-1];$p(_2P,_2O,$g($_.msg,_2O))}}$_=$__}function bwipp_gs1process(){if(!bwipp_gs1process.globals){var $__=$_;$_=bwipp_gs1process.globals={};$_.gs1process_primes=$a([2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83]);$_.gs1process_daysinmonth=$a([31,-1,31,30,31,30,31,31,30,31,30,31]);var _3=new Map([["cset","N"],["min",18],["max",18],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _6=new Map([["parts",$a([_3])],["dlpkey",$a([])]]);var _8=new Map([["cset","N"],["min",14],["max",14],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _E=new Map([["parts",$a([_8])],["ex",$a(["255","37"])],["dlpkey",$a([$a(["22","10","21"]),$a(["235"])])]]);var _G=new Map([["cset","N"],["min",14],["max",14],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _M=new Map([["parts",$a([_G])],["ex",$a(["01","03"])],["req",$a([$a([$a(["37"])])])]]);var _O=new Map([["cset","N"],["min",14],["max",14],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _R=new Map([["parts",$a([_O])],["ex",$a(["01","02","37"])],["dlattr",false]]);var _T=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _c=new Map([["parts",$a([_T])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["03"]),$a(["8006"]),$a(["8026"])])])]]);var _e=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmd0"])]]);var _n=new Map([["parts",$a([_e])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["03"]),$a(["8006"]),$a(["8026"])])])]]);var _p=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmd0"])]]);var _u=new Map([["parts",$a([_p])],["req",$a([$a([$a(["8020"])])])]]);var _w=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmd0"])]]);var _15=new Map([["parts",$a([_w])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["03"]),$a(["8006"]),$a(["8026"])])])]]);var _17=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmd0"])]]);var _1H=new Map([["parts",$a([_17])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["03"]),$a(["255"]),$a(["8006"]),$a(["8026"])])])]]);var _1J=new Map([["cset","N"],["min",2],["max",2],["opt",false],["linters",$a([])]]);var _1R=new Map([["parts",$a([_1J])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["8006"]),$a(["8026"])])])]]);var _1T=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _1b=new Map([["parts",$a([_1T])],["ex",$a(["235"])],["req",$a([$a([$a(["01"]),$a(["03"]),$a(["8006"])])])],["dlattr",false]]);var _1d=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _1i=new Map([["parts",$a([_1d])],["req",$a([$a([$a(["01"])])])],["dlattr",false]]);var _1k=new Map([["cset","X"],["min",1],["max",28],["opt",false],["linters",$a([])]]);var _1p=new Map([["parts",$a([_1k])],["req",$a([$a([$a(["01"])])])],["dlattr",false]]);var _1r=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _1z=new Map([["parts",$a([_1r])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["8006"]),$a(["8026"])])])]]);var _21=new Map([["cset","N"],["min",1],["max",6],["opt",false],["linters",$a([])]]);var _29=new Map([["parts",$a([_21])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["8006"]),$a(["8026"])])])]]);var _2B=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _2G=new Map([["parts",$a([_2B])],["req",$a([$a([$a(["01"])])])]]);var _2I=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _2Q=new Map([["parts",$a([_2I])],["req",$a([$a([$a(["01"]),$a(["8006"])]),$a([$a(["21"])])])]]);var _2S=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _2Y=new Map([["parts",$a([_2S])],["req",$a([$a([$a(["01"]),$a(["8006"])])])]]);var _2a=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _2c=new Map([["cset","X"],["min",1],["max",17],["opt",true],["linters",$a([])]]);var _2f=new Map([["parts",$a([_2a,_2c])],["dlpkey",$a([])]]);var _2h=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _2m=new Map([["parts",$a([_2h])],["req",$a([$a([$a(["414"])])])],["dlattr",false]]);var _2o=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _2q=new Map([["cset","N"],["min",1],["max",12],["opt",true],["linters",$a([])]]);var _2u=new Map([["parts",$a([_2o,_2q])],["ex",$a(["01","02","415","8006","8020","8026"])],["dlpkey",$a([])]]);var _2w=new Map([["cset","N"],["min",1],["max",8],["opt",false],["linters",$a([])]]);var _32=new Map([["parts",$a([_2w])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _34=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3B=new Map([["parts",$a([_34])],["ex",$a(["310n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3D=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3K=new Map([["parts",$a([_3D])],["ex",$a(["311n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3M=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3T=new Map([["parts",$a([_3M])],["ex",$a(["312n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3V=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3c=new Map([["parts",$a([_3V])],["ex",$a(["313n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3e=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3l=new Map([["parts",$a([_3e])],["ex",$a(["314n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3n=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _3u=new Map([["parts",$a([_3n])],["ex",$a(["315n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _3w=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _43=new Map([["parts",$a([_3w])],["ex",$a(["316n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _45=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4C=new Map([["parts",$a([_45])],["ex",$a(["320n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4E=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4L=new Map([["parts",$a([_4E])],["ex",$a(["321n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4N=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4U=new Map([["parts",$a([_4N])],["ex",$a(["322n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4W=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4d=new Map([["parts",$a([_4W])],["ex",$a(["323n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4f=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4m=new Map([["parts",$a([_4f])],["ex",$a(["324n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4o=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _4v=new Map([["parts",$a([_4o])],["ex",$a(["325n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _4x=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _54=new Map([["parts",$a([_4x])],["ex",$a(["326n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _56=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5D=new Map([["parts",$a([_56])],["ex",$a(["327n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _5F=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5M=new Map([["parts",$a([_5F])],["ex",$a(["328n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _5O=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5V=new Map([["parts",$a([_5O])],["ex",$a(["329n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _5X=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5e=new Map([["parts",$a([_5X])],["ex",$a(["330n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _5g=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5n=new Map([["parts",$a([_5g])],["ex",$a(["331n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _5p=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _5w=new Map([["parts",$a([_5p])],["ex",$a(["332n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _5y=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _65=new Map([["parts",$a([_5y])],["ex",$a(["333n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _67=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6E=new Map([["parts",$a([_67])],["ex",$a(["334n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _6G=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6N=new Map([["parts",$a([_6G])],["ex",$a(["335n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _6P=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6W=new Map([["parts",$a([_6P])],["ex",$a(["336n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _6Y=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6e=new Map([["parts",$a([_6Y])],["ex",$a(["337n"])],["req",$a([$a([$a(["01"])])])]]);var _6g=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6n=new Map([["parts",$a([_6g])],["ex",$a(["340n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _6p=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _6w=new Map([["parts",$a([_6p])],["ex",$a(["341n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _6y=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _75=new Map([["parts",$a([_6y])],["ex",$a(["342n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _77=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7E=new Map([["parts",$a([_77])],["ex",$a(["343n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7G=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7N=new Map([["parts",$a([_7G])],["ex",$a(["344n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7P=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7W=new Map([["parts",$a([_7P])],["ex",$a(["345n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7Y=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7f=new Map([["parts",$a([_7Y])],["ex",$a(["346n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7h=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7o=new Map([["parts",$a([_7h])],["ex",$a(["347n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7q=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _7x=new Map([["parts",$a([_7q])],["ex",$a(["348n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _7z=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _86=new Map([["parts",$a([_7z])],["ex",$a(["349n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _88=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8F=new Map([["parts",$a([_88])],["ex",$a(["350n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _8H=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8O=new Map([["parts",$a([_8H])],["ex",$a(["351n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _8Q=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8X=new Map([["parts",$a([_8Q])],["ex",$a(["352n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _8Z=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8g=new Map([["parts",$a([_8Z])],["ex",$a(["353n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _8i=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8p=new Map([["parts",$a([_8i])],["ex",$a(["354n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _8r=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _8y=new Map([["parts",$a([_8r])],["ex",$a(["355n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _90=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _97=new Map([["parts",$a([_90])],["ex",$a(["356n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _99=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9G=new Map([["parts",$a([_99])],["ex",$a(["357n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _9I=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9P=new Map([["parts",$a([_9I])],["ex",$a(["360n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _9R=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9Y=new Map([["parts",$a([_9R])],["ex",$a(["361n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _9a=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9h=new Map([["parts",$a([_9a])],["ex",$a(["362n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _9j=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9q=new Map([["parts",$a([_9j])],["ex",$a(["363n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _9s=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _9z=new Map([["parts",$a([_9s])],["ex",$a(["364n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _A1=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _A8=new Map([["parts",$a([_A1])],["ex",$a(["365n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _AA=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _AH=new Map([["parts",$a([_AA])],["ex",$a(["366n"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _AJ=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _AQ=new Map([["parts",$a([_AJ])],["ex",$a(["367n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _AS=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _AZ=new Map([["parts",$a([_AS])],["ex",$a(["368n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _Ab=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _Ai=new Map([["parts",$a([_Ab])],["ex",$a(["369n"])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _Ak=new Map([["cset","N"],["min",1],["max",8],["opt",false],["linters",$a([])]]);var _As=new Map([["parts",$a([_Ak])],["req",$a([$a([$a(["00"])]),$a([$a(["02"]),$a(["8026"])])])]]);var _Au=new Map([["cset","N"],["min",1],["max",15],["opt",false],["linters",$a([])]]);var _B1=new Map([["parts",$a([_Au])],["ex",$a(["390n","391n","394n","8111"])],["req",$a([$a([$a(["255"]),$a(["8020"])])])]]);var _B3=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso4217"])]]);var _B5=new Map([["cset","N"],["min",1],["max",15],["opt",false],["linters",$a([])]]);var _BB=new Map([["parts",$a([_B3,_B5])],["ex",$a(["391n"])],["req",$a([$a([$a(["8020"])])])]]);var _BD=new Map([["cset","N"],["min",1],["max",15],["opt",false],["linters",$a([])]]);var _BP=new Map([["parts",$a([_BD])],["ex",$a(["392n","393n"])],["req",$a([$a([$a(["01"])]),$a([$a(["30"]),$a(["31nn"]),$a(["32nn"]),$a(["35nn"]),$a(["36nn"])])])]]);var _BR=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso4217"])]]);var _BT=new Map([["cset","N"],["min",1],["max",15],["opt",false],["linters",$a([])]]);var _Bd=new Map([["parts",$a([_BR,_BT])],["ex",$a(["393n"])],["req",$a([$a([$a(["30"]),$a(["31nn"]),$a(["32nn"]),$a(["35nn"]),$a(["36nn"])])])]]);var _Bf=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a([])]]);var _Bl=new Map([["parts",$a([_Bf])],["ex",$a(["394n","8111"])],["req",$a([$a([$a(["255"])])])]]);var _Bn=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _Bx=new Map([["parts",$a([_Bn])],["ex",$a(["392n","393n","395n","8005"])],["req",$a([$a([$a(["30"]),$a(["31nn"]),$a(["32nn"]),$a(["35nn"]),$a(["36nn"])])])]]);var _Bz=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _C1=new Map([["parts",$a([_Bz])]]);var _C3=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a(["lintgcppos1"])]]);var _C6=new Map([["parts",$a([_C3])],["dlpkey",$a([])]]);var _C8=new Map([["cset","N"],["min",17],["max",17],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _CB=new Map([["parts",$a([_C8])],["dlpkey",$a([])]]);var _CD=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _CI=new Map([["parts",$a([_CD])],["req",$a([$a([$a(["00"])])])]]);var _CK=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _CM=new Map([["parts",$a([_CK])]]);var _CO=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _CT=new Map([["parts",$a([_CO])],["dlpkey",$a([$a(["254"]),$a(["7040"])])]]);var _CV=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _Cc=new Map([["parts",$a([_CV])],["req",$a([$a([$a(["8020"])])])],["dlpkey",$a([$a(["8020"])])]]);var _Ce=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _Cg=new Map([["parts",$a([_Ce])]]);var _Ci=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _Cm=new Map([["parts",$a([_Ci])],["dlpkey",$a([$a(["7040"])])]]);var _Co=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Cr=new Map([["parts",$a([_Co])],["ex",$a(["421"])]]);var _Ct=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _Cv=new Map([["cset","X"],["min",1],["max",9],["opt",false],["linters",$a([])]]);var _Cy=new Map([["parts",$a([_Ct,_Cv])],["ex",$a(["4307"])]]);var _D0=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _D9=new Map([["parts",$a([_D0])],["ex",$a(["426"])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["8006"]),$a(["8026"])])])]]);var _DB=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _DD=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _DF=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _DH=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _DJ=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _DQ=new Map([["parts",$a([_DB,_DD,_DF,_DH,_DJ])],["ex",$a(["426"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _DS=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _DZ=new Map([["parts",$a([_DS])],["ex",$a(["426"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Db=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _Dd=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _Df=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _Dh=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _Dj=new Map([["cset","N"],["min",3],["max",3],["opt",true],["linters",$a(["lintiso3166"])]]);var _Dq=new Map([["parts",$a([_Db,_Dd,_Df,_Dh,_Dj])],["ex",$a(["426"])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Ds=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166"])]]);var _Dy=new Map([["parts",$a([_Ds])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _E0=new Map([["cset","X"],["min",1],["max",3],["opt",false],["linters",$a([])]]);var _E8=new Map([["parts",$a([_E0])],["req",$a([$a([$a(["01"]),$a(["02"])]),$a([$a(["422"])])])]]);var _EA=new Map([["cset","X"],["min",1],["max",35],["opt",false],["linters",$a(["lintpcenc"])]]);var _EF=new Map([["parts",$a([_EA])],["req",$a([$a([$a(["00"])])])]]);var _EH=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _EM=new Map([["parts",$a([_EH])],["req",$a([$a([$a(["00"])])])]]);var _EO=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _ET=new Map([["parts",$a([_EO])],["req",$a([$a([$a(["4302"])])])]]);var _EV=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _Ea=new Map([["parts",$a([_EV])],["req",$a([$a([$a(["00"])])])]]);var _Ec=new Map([["cset","X"],["min",2],["max",2],["opt",false],["linters",$a(["lintiso3166alpha2"])]]);var _Eh=new Map([["parts",$a([_Ec])],["req",$a([$a([$a(["00"])])])]]);var _Ej=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _Eo=new Map([["parts",$a([_Ej])],["req",$a([$a([$a(["00"])])])]]);var _Eq=new Map([["cset","N"],["min",10],["max",10],["opt",false],["linters",$a(["lintlatitude"])]]);var _Es=new Map([["cset","N"],["min",10],["max",10],["opt",false],["linters",$a(["lintlongitude"])]]);var _Ex=new Map([["parts",$a([_Eq,_Es])],["req",$a([$a([$a(["00"])])])]]);var _Ez=new Map([["cset","X"],["min",1],["max",35],["opt",false],["linters",$a(["lintpcenc"])]]);var _F4=new Map([["parts",$a([_Ez])],["req",$a([$a([$a(["00"])])])]]);var _F6=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _FB=new Map([["parts",$a([_F6])],["req",$a([$a([$a(["00"])])])]]);var _FD=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _FI=new Map([["parts",$a([_FD])],["req",$a([$a([$a(["4312"])])])]]);var _FK=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _FP=new Map([["parts",$a([_FK])],["req",$a([$a([$a(["00"])])])]]);var _FR=new Map([["cset","X"],["min",2],["max",2],["opt",false],["linters",$a(["lintiso3166alpha2"])]]);var _FW=new Map([["parts",$a([_FR])],["req",$a([$a([$a(["00"])])])]]);var _FY=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Fd=new Map([["parts",$a([_FY])],["req",$a([$a([$a(["00"])])])]]);var _Ff=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _Fk=new Map([["parts",$a([_Ff])],["req",$a([$a([$a(["00"])])])]]);var _Fm=new Map([["cset","X"],["min",1],["max",35],["opt",false],["linters",$a(["lintpcenc"])]]);var _Fr=new Map([["parts",$a([_Fm])],["req",$a([$a([$a(["00"])])])]]);var _Ft=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a(["lintyesno"])]]);var _Fy=new Map([["parts",$a([_Ft])],["req",$a([$a([$a(["00"])])])]]);var _G0=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmd0"])]]);var _G2=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["linthhmi"])]]);var _G7=new Map([["parts",$a([_G0,_G2])],["req",$a([$a([$a(["00"])])])]]);var _G9=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _GE=new Map([["parts",$a([_G9])],["req",$a([$a([$a(["00"])])])]]);var _GG=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _GI=new Map([["cset","X"],["min",1],["max",1],["opt",true],["linters",$a(["linthyphen"])]]);var _GO=new Map([["parts",$a([_GG,_GI])],["ex",$a(["4331"])],["req",$a([$a([$a(["00"])])])]]);var _GQ=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _GS=new Map([["cset","X"],["min",1],["max",1],["opt",true],["linters",$a(["linthyphen"])]]);var _GY=new Map([["parts",$a([_GQ,_GS])],["ex",$a(["4330"])],["req",$a([$a([$a(["00"])])])]]);var _Ga=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _Gc=new Map([["cset","X"],["min",1],["max",1],["opt",true],["linters",$a(["linthyphen"])]]);var _Gi=new Map([["parts",$a([_Ga,_Gc])],["ex",$a(["4333"])],["req",$a([$a([$a(["00"])])])]]);var _Gk=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _Gm=new Map([["cset","X"],["min",1],["max",1],["opt",true],["linters",$a(["linthyphen"])]]);var _Gs=new Map([["parts",$a([_Gk,_Gm])],["ex",$a(["4332"])],["req",$a([$a([$a(["00"])])])]]);var _Gu=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a([])]]);var _H2=new Map([["parts",$a([_Gu])],["req",$a([$a([$a(["01"]),$a(["02"]),$a(["8006"]),$a(["8026"])])])]]);var _H4=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _HA=new Map([["parts",$a([_H4])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _HC=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _HE=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["linthhmi"])]]);var _HK=new Map([["parts",$a([_HC,_HE])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _HM=new Map([["cset","N"],["min",1],["max",4],["opt",false],["linters",$a([])]]);var _HR=new Map([["parts",$a([_HM])],["req",$a([$a([$a(["01","10"])])])]]);var _HT=new Map([["cset","X"],["min",1],["max",12],["opt",false],["linters",$a([])]]);var _HZ=new Map([["parts",$a([_HT])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Hb=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _Hh=new Map([["parts",$a([_Hb])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Hj=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _Hl=new Map([["cset","N"],["min",6],["max",6],["opt",true],["linters",$a(["lintyymmdd"])]]);var _Hr=new Map([["parts",$a([_Hj,_Hl])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Ht=new Map([["cset","X"],["min",1],["max",3],["opt",false],["linters",$a([])]]);var _Hz=new Map([["parts",$a([_Ht])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _I1=new Map([["cset","X"],["min",1],["max",10],["opt",false],["linters",$a([])]]);var _I7=new Map([["parts",$a([_I1])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _I9=new Map([["cset","X"],["min",1],["max",2],["opt",false],["linters",$a([])]]);var _IF=new Map([["parts",$a([_I9])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _IH=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _IJ=new Map([["cset","N"],["min",4],["max",4],["opt",true],["linters",$a(["linthhmi"])]]);var _IP=new Map([["parts",$a([_IH,_IJ])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _IR=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _IZ=new Map([["parts",$a([_IR])],["req",$a([$a([$a(["01"]),$a(["8006"])]),$a([$a(["416"])])])]]);var _Ib=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Ih=new Map([["parts",$a([_Ib])],["req",$a([$a([$a(["01"]),$a(["8006"])])])]]);var _Ij=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Io=new Map([["parts",$a([_Ij])],["req",$a([$a([$a(["7021"])])])]]);var _Iq=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a(["lintgcppos1"])]]);var _Is=new Map([["parts",$a([_Iq])]]);var _Iu=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintiso3166999"])]]);var _Iw=new Map([["cset","X"],["min",1],["max",27],["opt",false],["linters",$a([])]]);var _J2=new Map([["parts",$a([_Iu,_Iw])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _J4=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a([])]]);var _J6=new Map([["cset","X"],["min",1],["max",1],["opt",false],["linters",$a([])]]);var _J8=new Map([["cset","X"],["min",1],["max",1],["opt",false],["linters",$a([])]]);var _JA=new Map([["cset","X"],["min",1],["max",1],["opt",false],["linters",$a(["lintimporteridx"])]]);var _JC=new Map([["parts",$a([_J4,_J6,_J8,_JA])],["dlattr",false]]);var _JE=new Map([["cset","X"],["min",1],["max",4],["opt",false],["linters",$a(["lintpackagetype"])]]);var _JJ=new Map([["parts",$a([_JE])],["req",$a([$a([$a(["00"])])])],["dlattr",false]]);var _JL=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _JQ=new Map([["parts",$a([_JL])],["req",$a([$a([$a(["01"])])])]]);var _JS=new Map([["cset","X"],["min",2],["max",2],["opt",false],["linters",$a([])]]);var _JU=new Map([["cset","X"],["min",1],["max",28],["opt",false],["linters",$a([])]]);var _Ja=new Map([["parts",$a([_JS,_JU])],["req",$a([$a([$a(["01"]),$a(["8004"])])])]]);var _Jc=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Ji=new Map([["parts",$a([_Jc])],["req",$a([$a([$a(["01"]),$a(["8006"])])])]]);var _Jk=new Map([["cset","N"],["min",2],["max",2],["opt",false],["linters",$a(["lintmediatype"])]]);var _Jq=new Map([["parts",$a([_Jk])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])]]);var _Js=new Map([["cset","X"],["min",1],["max",25],["opt",false],["linters",$a([])]]);var _Jy=new Map([["parts",$a([_Js])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])]]);var _K0=new Map([["cset","N"],["min",8],["max",8],["opt",false],["linters",$a(["lintyyyymmdd"])]]);var _K6=new Map([["parts",$a([_K0])],["ex",$a(["7251"])],["req",$a([$a([$a(["8018"])])])]]);var _K8=new Map([["cset","N"],["min",8],["max",8],["opt",false],["linters",$a(["lintyyyymmdd"])]]);var _KA=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["linthhmi"])]]);var _KG=new Map([["parts",$a([_K8,_KA])],["ex",$a(["7250"])],["req",$a([$a([$a(["8018"])])])]]);var _KI=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a(["lintiso5218"])]]);var _KN=new Map([["parts",$a([_KI])],["req",$a([$a([$a(["8018"])])])]]);var _KP=new Map([["cset","X"],["min",1],["max",40],["opt",false],["linters",$a(["lintpcenc"])]]);var _KW=new Map([["parts",$a([_KP])],["ex",$a(["7256","7259"])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])]]);var _KY=new Map([["cset","X"],["min",1],["max",10],["opt",false],["linters",$a([])]]);var _Kf=new Map([["parts",$a([_KY])],["ex",$a(["7256","7259"])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])]]);var _Kh=new Map([["cset","X"],["min",1],["max",90],["opt",false],["linters",$a(["lintpcenc"])]]);var _Kn=new Map([["parts",$a([_Kh])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])]]);var _Kp=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintpcenc"])]]);var _Ku=new Map([["parts",$a([_Kp])],["req",$a([$a([$a(["8018"])])])]]);var _Kw=new Map([["cset","X"],["min",3],["max",3],["opt",false],["linters",$a(["lintposinseqslash"])]]);var _L1=new Map([["parts",$a([_Kw])],["req",$a([$a([$a(["8018","7259"])])])]]);var _L3=new Map([["cset","X"],["min",1],["max",40],["opt",false],["linters",$a(["lintpcenc"])]]);var _L9=new Map([["parts",$a([_L3])],["ex",$a(["7256"])],["req",$a([$a([$a(["8018"])])])]]);var _LB=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["lintnonzero"])]]);var _LD=new Map([["cset","N"],["min",5],["max",5],["opt",false],["linters",$a(["lintnonzero"])]]);var _LF=new Map([["cset","N"],["min",3],["max",3],["opt",false],["linters",$a(["lintnonzero"])]]);var _LH=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a(["lintwinding"])]]);var _LJ=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a([])]]);var _LO=new Map([["parts",$a([_LB,_LD,_LF,_LH,_LJ])],["req",$a([$a([$a(["01"])])])]]);var _LQ=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _LS=new Map([["parts",$a([_LQ])]]);var _LU=new Map([["cset","N"],["min",1],["max",1],["opt",false],["linters",$a(["lintzero"])]]);var _LW=new Map([["cset","N"],["min",13],["max",13],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _LY=new Map([["cset","X"],["min",1],["max",16],["opt",true],["linters",$a([])]]);var _Lb=new Map([["parts",$a([_LU,_LW,_LY])],["dlpkey",$a([])]]);var _Ld=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a(["lintgcppos1"])]]);var _Lh=new Map([["parts",$a([_Ld])],["dlpkey",$a([$a(["7040"])])]]);var _Lj=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a([])]]);var _Lp=new Map([["parts",$a([_Lj])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _Lr=new Map([["cset","N"],["min",14],["max",14],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _Lt=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["lintpieceoftotal"])]]);var _Ly=new Map([["parts",$a([_Lr,_Lt])],["ex",$a(["01","37"])],["dlpkey",$a([$a(["22","10","21"])])]]);var _M0=new Map([["cset","X"],["min",1],["max",34],["opt",false],["linters",$a(["lintiban"])]]);var _M5=new Map([["parts",$a([_M0])],["req",$a([$a([$a(["415"])])])]]);var _M7=new Map([["cset","N"],["min",6],["max",6],["opt",false],["linters",$a(["lintyymmdd"])]]);var _M9=new Map([["cset","N"],["min",2],["max",2],["opt",false],["linters",$a(["linthh"])]]);var _MB=new Map([["cset","N"],["min",2],["max",2],["opt",true],["linters",$a(["lintmi"])]]);var _MD=new Map([["cset","N"],["min",2],["max",2],["opt",true],["linters",$a(["lintss"])]]);var _MJ=new Map([["parts",$a([_M7,_M9,_MB,_MD])],["req",$a([$a([$a(["01"]),$a(["02"])])])]]);var _ML=new Map([["cset","X"],["min",1],["max",50],["opt",false],["linters",$a([])]]);var _MR=new Map([["parts",$a([_ML])],["req",$a([$a([$a(["00"]),$a(["01"])])])]]);var _MT=new Map([["cset","Y"],["min",1],["max",30],["opt",false],["linters",$a(["lintgcppos1"])]]);var _MX=new Map([["parts",$a([_MT])],["dlpkey",$a([$a(["8011"])])]]);var _MZ=new Map([["cset","N"],["min",1],["max",12],["opt",false],["linters",$a(["lintnozeroprefix"])]]);var _Me=new Map([["parts",$a([_MZ])],["req",$a([$a([$a(["8010"])])])],["dlattr",false]]);var _Mg=new Map([["cset","X"],["min",1],["max",20],["opt",false],["linters",$a([])]]);var _Mm=new Map([["parts",$a([_Mg])],["req",$a([$a([$a(["01"]),$a(["8006"])])])]]);var _Mo=new Map([["cset","X"],["min",1],["max",25],["opt",false],["linters",$a(["lintcsumalpha","lintgcppos1"])]]);var _Mr=new Map([["parts",$a([_Mo])],["dlpkey",$a([])]]);var _Mt=new Map([["cset","X"],["min",1],["max",25],["opt",false],["linters",$a(["lintcsumalpha","lintgcppos1","linthasnondigit"])]]);var _My=new Map([["parts",$a([_Mt])],["req",$a([$a([$a(["01"])])])],["dlattr",false]]);var _N0=new Map([["cset","N"],["min",18],["max",18],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _N5=new Map([["parts",$a([_N0])],["ex",$a(["8018"])],["dlpkey",$a([$a(["8019"])])]]);var _N7=new Map([["cset","N"],["min",18],["max",18],["opt",false],["linters",$a(["lintcsum","lintgcppos1"])]]);var _NC=new Map([["parts",$a([_N7])],["ex",$a(["8017"])],["dlpkey",$a([$a(["8019"])])]]);var _NE=new Map([["cset","N"],["min",1],["max",10],["opt",false],["linters",$a([])]]);var _NK=new Map([["parts",$a([_NE])],["req",$a([$a([$a(["8017"]),$a(["8018"])])])],["dlattr",false]]);var _NM=new Map([["cset","X"],["min",1],["max",25],["opt",false],["linters",$a([])]]);var _NR=new Map([["parts",$a([_NM])],["req",$a([$a([$a(["415"])])])],["dlattr",false]]);var _NT=new Map([["cset","N"],["min",14],["max",14],["opt",false],["linters",$a(["lintcsum","lintgcppos2"])]]);var _NV=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a(["lintpieceoftotal"])]]);var _Nb=new Map([["parts",$a([_NT,_NV])],["ex",$a(["02","8006"])],["req",$a([$a([$a(["37"])])])]]);var _Nd=new Map([["cset","Z"],["min",1],["max",90],["opt",false],["linters",$a([])]]);var _Nr=new Map([["parts",$a([_Nd])],["req",$a([$a([$a(["00"]),$a(["01","21"]),$a(["253"]),$a(["255"]),$a(["8003"]),$a(["8004"]),$a(["8006","21"]),$a(["8010","8011"]),$a(["8017"]),$a(["8018"])])])]]);var _Nt=new Map([["cset","N"],["min",15],["max",15],["opt",false],["linters",$a([])]]);var _Ny=new Map([["parts",$a([_Nt])],["req",$a([$a([$a(["01","21"])])])],["dlattr",false]]);var _O0=new Map([["cset","N"],["min",15],["max",15],["opt",false],["linters",$a([])]]);var _O5=new Map([["parts",$a([_O0])],["req",$a([$a([$a(["01","21","8040"])])])],["dlattr",false]]);var _O7=new Map([["cset","N"],["min",32],["max",32],["opt",false],["linters",$a([])]]);var _OC=new Map([["parts",$a([_O7])],["req",$a([$a([$a(["01","21","8040"])])])],["dlattr",false]]);var _OE=new Map([["cset","N"],["min",18],["max",18],["opt",false],["linters",$a([])]]);var _OG=new Map([["cset","N"],["min",1],["max",2],["opt",true],["linters",$a([])]]);var _OL=new Map([["parts",$a([_OE,_OG])],["req",$a([$a([$a(["01","21","8040"])])])],["dlattr",false]]);var _ON=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintcouponcode"])]]);var _OP=new Map([["parts",$a([_ON])]]);var _OR=new Map([["cset","N"],["min",4],["max",4],["opt",false],["linters",$a([])]]);var _OW=new Map([["parts",$a([_OR])],["req",$a([$a([$a(["255"])])])]]);var _OY=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a(["lintcouponposoffer"])]]);var _Oa=new Map([["parts",$a([_OY])]]);var _Oc=new Map([["cset","X"],["min",1],["max",70],["opt",false],["linters",$a([])]]);var _Oh=new Map([["parts",$a([_Oc])],["req",$a([$a([$a(["01"])])])],["dlattr",false]]);var _Oj=new Map([["cset","X"],["min",1],["max",30],["opt",false],["linters",$a([])]]);var _Ol=new Map([["parts",$a([_Oj])]]);var _On=new Map([["cset","X"],["min",1],["max",90],["opt",false],["linters",$a([])]]);var _Op=new Map([["parts",$a([_On])]]);var _Oq=new Map([["00",_6],["01",_E],["02",_M],["03",_R],["10",_c],["11",_n],["12",_u],["13",_15],["15",_15],["16",_15],["17",_1H],["20",_1R],["21",_1b],["22",_1i],["235",_1p],["240",_1z],["241",_1z],["242",_29],["243",_2G],["250",_2Q],["251",_2Y],["253",_2f],["254",_2m],["255",_2u],["30",_32],["3100",_3B],["3101",_3B],["3102",_3B],["3103",_3B],["3104",_3B],["3105",_3B],["3110",_3K],["3111",_3K],["3112",_3K],["3113",_3K],["3114",_3K],["3115",_3K],["3120",_3T],["3121",_3T],["3122",_3T],["3123",_3T],["3124",_3T],["3125",_3T],["3130",_3c],["3131",_3c],["3132",_3c],["3133",_3c],["3134",_3c],["3135",_3c],["3140",_3l],["3141",_3l],["3142",_3l],["3143",_3l],["3144",_3l],["3145",_3l],["3150",_3u],["3151",_3u],["3152",_3u],["3153",_3u],["3154",_3u],["3155",_3u],["3160",_43],["3161",_43],["3162",_43],["3163",_43],["3164",_43],["3165",_43],["3200",_4C],["3201",_4C],["3202",_4C],["3203",_4C],["3204",_4C],["3205",_4C],["3210",_4L],["3211",_4L],["3212",_4L],["3213",_4L],["3214",_4L],["3215",_4L],["3220",_4U],["3221",_4U],["3222",_4U],["3223",_4U],["3224",_4U],["3225",_4U],["3230",_4d],["3231",_4d],["3232",_4d],["3233",_4d],["3234",_4d],["3235",_4d],["3240",_4m],["3241",_4m],["3242",_4m],["3243",_4m],["3244",_4m],["3245",_4m],["3250",_4v],["3251",_4v],["3252",_4v],["3253",_4v],["3254",_4v],["3255",_4v],["3260",_54],["3261",_54],["3262",_54],["3263",_54],["3264",_54],["3265",_54],["3270",_5D],["3271",_5D],["3272",_5D],["3273",_5D],["3274",_5D],["3275",_5D],["3280",_5M],["3281",_5M],["3282",_5M],["3283",_5M],["3284",_5M],["3285",_5M],["3290",_5V],["3291",_5V],["3292",_5V],["3293",_5V],["3294",_5V],["3295",_5V],["3300",_5e],["3301",_5e],["3302",_5e],["3303",_5e],["3304",_5e],["3305",_5e],["3310",_5n],["3311",_5n],["3312",_5n],["3313",_5n],["3314",_5n],["3315",_5n],["3320",_5w],["3321",_5w],["3322",_5w],["3323",_5w],["3324",_5w],["3325",_5w],["3330",_65],["3331",_65],["3332",_65],["3333",_65],["3334",_65],["3335",_65],["3340",_6E],["3341",_6E],["3342",_6E],["3343",_6E],["3344",_6E],["3345",_6E],["3350",_6N],["3351",_6N],["3352",_6N],["3353",_6N],["3354",_6N],["3355",_6N],["3360",_6W],["3361",_6W],["3362",_6W],["3363",_6W],["3364",_6W],["3365",_6W],["3370",_6e],["3371",_6e],["3372",_6e],["3373",_6e],["3374",_6e],["3375",_6e],["3400",_6n],["3401",_6n],["3402",_6n],["3403",_6n],["3404",_6n],["3405",_6n],["3410",_6w],["3411",_6w],["3412",_6w],["3413",_6w],["3414",_6w],["3415",_6w],["3420",_75],["3421",_75],["3422",_75],["3423",_75],["3424",_75],["3425",_75],["3430",_7E],["3431",_7E],["3432",_7E],["3433",_7E],["3434",_7E],["3435",_7E],["3440",_7N],["3441",_7N],["3442",_7N],["3443",_7N],["3444",_7N],["3445",_7N],["3450",_7W],["3451",_7W],["3452",_7W],["3453",_7W],["3454",_7W],["3455",_7W],["3460",_7f],["3461",_7f],["3462",_7f],["3463",_7f],["3464",_7f],["3465",_7f],["3470",_7o],["3471",_7o],["3472",_7o],["3473",_7o],["3474",_7o],["3475",_7o],["3480",_7x],["3481",_7x],["3482",_7x],["3483",_7x],["3484",_7x],["3485",_7x],["3490",_86],["3491",_86],["3492",_86],["3493",_86],["3494",_86],["3495",_86],["3500",_8F],["3501",_8F],["3502",_8F],["3503",_8F],["3504",_8F],["3505",_8F],["3510",_8O],["3511",_8O],["3512",_8O],["3513",_8O],["3514",_8O],["3515",_8O],["3520",_8X],["3521",_8X],["3522",_8X],["3523",_8X],["3524",_8X],["3525",_8X],["3530",_8g],["3531",_8g],["3532",_8g],["3533",_8g],["3534",_8g],["3535",_8g],["3540",_8p],["3541",_8p],["3542",_8p],["3543",_8p],["3544",_8p],["3545",_8p],["3550",_8y],["3551",_8y],["3552",_8y],["3553",_8y],["3554",_8y],["3555",_8y],["3560",_97],["3561",_97],["3562",_97],["3563",_97],["3564",_97],["3565",_97],["3570",_9G],["3571",_9G],["3572",_9G],["3573",_9G],["3574",_9G],["3575",_9G],["3600",_9P],["3601",_9P],["3602",_9P],["3603",_9P],["3604",_9P],["3605",_9P],["3610",_9Y],["3611",_9Y],["3612",_9Y],["3613",_9Y],["3614",_9Y],["3615",_9Y],["3620",_9h],["3621",_9h],["3622",_9h],["3623",_9h],["3624",_9h],["3625",_9h],["3630",_9q],["3631",_9q],["3632",_9q],["3633",_9q],["3634",_9q],["3635",_9q],["3640",_9z],["3641",_9z],["3642",_9z],["3643",_9z],["3644",_9z],["3645",_9z],["3650",_A8],["3651",_A8],["3652",_A8],["3653",_A8],["3654",_A8],["3655",_A8],["3660",_AH],["3661",_AH],["3662",_AH],["3663",_AH],["3664",_AH],["3665",_AH],["3670",_AQ],["3671",_AQ],["3672",_AQ],["3673",_AQ],["3674",_AQ],["3675",_AQ],["3680",_AZ],["3681",_AZ],["3682",_AZ],["3683",_AZ],["3684",_AZ],["3685",_AZ],["3690",_Ai],["3691",_Ai],["3692",_Ai],["3693",_Ai],["3694",_Ai],["3695",_Ai],["37",_As],["3900",_B1],["3901",_B1],["3902",_B1],["3903",_B1],["3904",_B1],["3905",_B1],["3906",_B1],["3907",_B1],["3908",_B1],["3909",_B1],["3910",_BB],["3911",_BB],["3912",_BB],["3913",_BB],["3914",_BB],["3915",_BB],["3916",_BB],["3917",_BB],["3918",_BB],["3919",_BB],["3920",_BP],["3921",_BP],["3922",_BP],["3923",_BP],["3924",_BP],["3925",_BP],["3926",_BP],["3927",_BP],["3928",_BP],["3929",_BP],["3930",_Bd],["3931",_Bd],["3932",_Bd],["3933",_Bd],["3934",_Bd],["3935",_Bd],["3936",_Bd],["3937",_Bd],["3938",_Bd],["3939",_Bd],["3940",_Bl],["3941",_Bl],["3942",_Bl],["3943",_Bl],["3950",_Bx],["3951",_Bx],["3952",_Bx],["3953",_Bx],["3954",_Bx],["3955",_Bx],["400",_C1],["401",_C6],["402",_CB],["403",_CI],["410",_CM],["411",_CM],["412",_CM],["413",_CM],["414",_CT],["415",_Cc],["416",_Cg],["417",_Cm],["420",_Cr],["421",_Cy],["422",_D9],["423",_DQ],["424",_DZ],["425",_Dq],["426",_Dy],["427",_E8],["4300",_EF],["4301",_EF],["4302",_EM],["4303",_ET],["4304",_Ea],["4305",_Ea],["4306",_Ea],["4307",_Eh],["4308",_Eo],["4309",_Ex],["4310",_F4],["4311",_F4],["4312",_FB],["4313",_FI],["4314",_FP],["4315",_FP],["4316",_FP],["4317",_FW],["4318",_Fd],["4319",_Fk],["4320",_Fr],["4321",_Fy],["4322",_Fy],["4323",_Fy],["4324",_G7],["4325",_G7],["4326",_GE],["4330",_GO],["4331",_GY],["4332",_Gi],["4333",_Gs],["7001",_H2],["7002",_HA],["7003",_HK],["7004",_HR],["7005",_HZ],["7006",_Hh],["7007",_Hr],["7008",_Hz],["7009",_I7],["7010",_IF],["7011",_IP],["7020",_IZ],["7021",_Ih],["7022",_Io],["7023",_Is],["7030",_J2],["7031",_J2],["7032",_J2],["7033",_J2],["7034",_J2],["7035",_J2],["7036",_J2],["7037",_J2],["7038",_J2],["7039",_J2],["7040",_JC],["7041",_JJ],["710",_JQ],["711",_JQ],["712",_JQ],["713",_JQ],["714",_JQ],["715",_JQ],["716",_JQ],["717",_JQ],["7230",_Ja],["7231",_Ja],["7232",_Ja],["7233",_Ja],["7234",_Ja],["7235",_Ja],["7236",_Ja],["7237",_Ja],["7238",_Ja],["7239",_Ja],["7240",_Ji],["7241",_Jq],["7242",_Jy],["7250",_K6],["7251",_KG],["7252",_KN],["7253",_KW],["7254",_KW],["7255",_Kf],["7256",_Kn],["7257",_Ku],["7258",_L1],["7259",_L9],["8001",_LO],["8002",_LS],["8003",_Lb],["8004",_Lh],["8005",_Lp],["8006",_Ly],["8007",_M5],["8008",_MJ],["8009",_MR],["8010",_MX],["8011",_Me],["8012",_Mm],["8013",_Mr],["8014",_My],["8017",_N5],["8018",_NC],["8019",_NK],["8020",_NR],["8026",_Nb],["8030",_Nr],["8040",_Ny],["8041",_O5],["8042",_OC],["8043",_OL],["8110",_OP],["8111",_OW],["8112",_Oa],["8200",_Oh],["90",_Ol],["91",_Op],["92",_Op],["93",_Op],["94",_Op],["95",_Op],["96",_Op],["97",_Op],["98",_Op],["99",_Op]]);$_.gs1syntax=_Oq;$k[$j++]=Infinity;var _Or=$a(["00","01","02","03","04","11","12","13","14","15","16","17","18","19","20","31","32","33","34","35","36","41"]);for(var _Os=0,_Ot=_Or.length;_Os<_Ot;_Os++){var _Ou=$g(_Or,_Os);$k[$j++]=_Ou;$k[$j++]=_Ou}var _Ov=$d();$_.aifixed=_Ov;$k[$j++]=Infinity;$k[$j++]=0;for(var _Ow=0,_Ox="!\"%&'()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".length;_Ow<_Ox;_Ow++){var _Oy=$g("!\"%&'()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",_Ow);var _Oz=$k[--$j];$k[$j++]=_Oy;$k[$j++]=_Oz;$k[$j++]=_Oz+1}$j--;var _P0=$d();$_.cset82=_P0;$k[$j++]=Infinity;$k[$j++]=0;for(var _P1=0,_P2="#-/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".length;_P1<_P2;_P1++){var _P4=$k[--$j];$k[$j++]=$g("#-/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",_P1);$k[$j++]=_P4;$k[$j++]=_P4+1}$j--;var _P5=$d();$_.cset39=_P5;$k[$j++]=Infinity;$k[$j++]=0;for(var _P6=0,_P7="23456789ABCDEFGHJKLMNPQRSTUVWXYZ".length;_P6<_P7;_P6++){var _P9=$k[--$j];$k[$j++]=$g("23456789ABCDEFGHJKLMNPQRSTUVWXYZ",_P6);$k[$j++]=_P9;$k[$j++]=_P9+1}$j--;var _PA=$d();$_.cset32=_PA;$k[$j++]=Infinity;$k[$j++]=0;for(var _PB=0,_PC="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".length;_PB<_PC;_PB++){var _PE=$k[--$j];$k[$j++]=$g("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",_PB);$k[$j++]=_PE;$k[$j++]=_PE+1}$j--;var _PF=$d();$_.cset64=_PF;$k[$j++]=Infinity;var _PG=$a(["004","008","010","012","016","020","024","028","031","032","036","040","044","048","050","051","052","056","060","064","068","070","072","074","076","084","086","090","092","096","100","104","108","112","116","120","124","132","136","140","144","148","152","156","158","162","166","170","174","175","178","180","184","188","191","192","196","203","204","208","212","214","218","222","226","231","232","233","234","238","239","242","246","248","250","254","258","260","262","266","268","270","275","276","288","292","296","300","304","308","312","316","320","324","328","332","334","336","340","344","348","352","356","360","364","368","372","376","380","384","388","392","398","400","404","408","410","414","417","418","422","426","428","430","434","438","440","442","446","450","454","458","462","466","470","474","478","480","484","492","496","498","499","500","504","508","512","516","520","524","528","531","533","534","535","540","548","554","558","562","566","570","574","578","580","581","583","584","585","586","591","598","600","604","608","612","616","620","624","626","630","634","638","642","643","646","652","654","659","660","662","663","666","670","674","678","682","686","688","690","694","702","703","704","705","706","710","716","724","728","729","732","740","744","748","752","756","760","762","764","768","772","776","780","784","788","792","795","796","798","800","804","807","818","826","831","832","833","834","840","850","854","858","860","862","876","882","887","894"]);for(var _PH=0,_PI=_PG.length;_PH<_PI;_PH++){var _PJ=$g(_PG,_PH);$k[$j++]=_PJ;$k[$j++]=_PJ}var _PK=$d();$_.iso3166=_PK;$k[$j++]=Infinity;var _PL=$a(["AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BQ","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CU","CV","CW","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","GB","GD","GE","GF","GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MF","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SV","SX","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","YE","YT","ZA","ZM","ZW"]);for(var _PM=0,_PN=_PL.length;_PM<_PN;_PM++){var _PO=$g(_PL,_PM);$k[$j++]=_PO;$k[$j++]=_PO}var _PP=$d();$_.iso3166alpha2=_PP;$k[$j++]=Infinity;var _PQ=$a(["008","012","032","036","044","048","050","051","052","060","064","068","072","084","090","096","104","108","116","124","132","136","144","152","156","170","174","188","191","192","203","208","214","222","230","232","238","242","262","270","292","320","324","328","332","340","344","348","352","356","360","364","368","376","388","392","396","398","400","404","408","410","414","417","418","422","426","430","434","446","454","458","462","480","484","496","498","504","512","516","524","532","533","548","554","558","566","578","586","590","598","600","604","608","634","643","646","654","682","690","694","702","704","706","710","728","748","752","756","760","764","776","780","784","788","800","807","818","826","834","840","858","860","882","886","901","925","927","928","929","930","931","932","933","934","936","938","940","941","943","944","946","947","948","949","950","951","952","953","955","956","957","958","959","960","961","962","963","964","965","967","968","969","970","971","972","973","975","976","977","978","979","980","981","984","985","986","990","994","997","999"]);for(var _PR=0,_PS=_PQ.length;_PR<_PS;_PR++){var _PT=$g(_PQ,_PR);$k[$j++]=_PT;$k[$j++]=_PT}var _PU=$d();$_.iso4217=_PU;$k[$j++]=Infinity;var _PV=$a(["01","02","03","04","05","06","07","08","09","10","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95","96","97","98","99"]);for(var _PW=0,_PX=_PV.length;_PW<_PX;_PW++){var _PY=$g(_PV,_PW);$k[$j++]=_PY;$k[$j++]=_PY}var _PZ=$d();$_.aidcmediatype=_PZ;$k[$j++]=Infinity;var _Pa=$a(["1A","1B","1D","1F","1G","1W","200","201","202","203","204","205","206","210","211","212","2C","3A","3H","43","44","4A","4B","4C","4D","4F","4G","4H","5H","5L","5M","6H","6P","7A","7B","8","8A","8B","8C","9","AA","AB","AC","AD","AF","AG","AH","AI","AJ","AL","AM","AP","APE/AT","AV","B4","BB","BC","BD","BE","BF","BG","BGE","BH","BI","BJ","BK","BL","BM","BME","BN","BO","BP","BQ","BR","BRI","BS","BT","BU","BV","BW","BX","BY","BZ","CA","CB","CBL","CC","CCE","CD","CE","CF","CG","CH","CI","CJ","CK","CL","CM","CN","CO","CP","CQ","CR","CS","CT","CU","CV","CW","CX","CY","CZ","DA","DB","DC","DG","DH","DI","DJ","DK","DL","DM","DN","DP","DPE","DR","DS","DT","DU","DV","DW","DX","DY","E1","E2","E3","EC","ED","EE","EF","EG","EH","EI","EN","FB","FC","FD","FE","FI","FL","FO","FOB","FP","FPE","FR","FT","FW","FX","GB","GI","GL","GR","GU","GY","GZ","HA","HB","HC","HG","HN","HR","IA","IB","IC","ID","IE","IF","IG","IH","IK","IL","IN","IZ","JB","JC","JG","JR","JT","JY","KG","KI","LAB","LE","LG","LT","LU","LV","LZ","MA","MB","MC","ME","MPE","MR","MS","MT","MW","MX","NA","NE","NF","NG","NS","NT","NU","NV","OA","OB","OC","OD","OE","OF","OK","OPE","OT","OU","P2","PA","PAE","PB","PC","PD","PE","PF","PG","PH","PI","PJ","PK","PL","PLP","PN","PO","POP","PP","PPE","PR","PT","PU","PUE","PV","PX","PY","PZ","QA","QB","QC","QD","QF","QG","QH","QJ","QK","QL","QM","QN","QP","QQ","QR","QS","RB1","RB2","RB3","RCB","RD","RG","RJ","RK","RL","RO","RT","RZ","S1","SA","SB","SC","SD","SE","SEC","SH","SI","SK","SL","SM","SO","SP","SS","ST","STL","SU","SV","SW","SX","SY","SZ","T1","TB","TC","TD","TE","TEV","TG","THE","TI","TK","TL","TN","TO","TR","TRE","TS","TT","TTE","TU","TV","TW","TWE","TY","TZ","UC","UN","UUE","VA","VG","VI","VK","VL","VN","VO","VP","VQ","VR","VS","VY","WA","WB","WC","WD","WF","WG","WH","WJ","WK","WL","WM","WN","WP","WQ","WR","WRP","WS","WT","WU","WV","WW","WX","WY","WZ","X11","X12","X15","X16","X17","X18","X19","X20","X3","XA","XB","XC","XD","XF","XG","XH","XJ","XK","YA","YB","YC","YD","YF","YG","YH","YJ","YK","YL","YM","YN","YP","YQ","YR","YS","YT","YV","YW","YX","YY","YZ","ZA","ZB","ZC","ZD","ZF","ZG","ZH","ZJ","ZK","ZL","ZM","ZN","ZP","ZQ","ZR","ZS","ZT","ZU","ZV","ZW","ZX","ZY","ZZ"]);for(var _Pb=0,_Pc=_Pa.length;_Pb<_Pc;_Pb++){var _Pd=$g(_Pa,_Pb);$k[$j++]=_Pd;$k[$j++]=_Pd}var _Pe=$d();$_.packagetype=_Pe;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_gs1process.globals);var _Pf=$k[--$j];if($eq(_Pf,"ai")){var _Pg=$k[--$j];$ax(_Pg,"(");var _Ph=$k[--$j];if(!_Ph){$k[$j-1]="bwipp.GS1aiMissingOpenParen#2920";$k[$j++]="AIs must start with '('";bwipp_raiseerror()}$j--;var _Pj=$k[--$j];$k[$j++]=Infinity;$k[$j++]=_Pj;for(;;){var _Pk=$k[$j-1];if(_Pk.length==0){$j--;break}var _Pl=$k[--$j];$X(_Pl,")");var _Pm=$k[--$j];if(!_Pm){$l();$k[$j++]="bwipp.GS1aiMissingCloseParen#2926";$k[$j++]="AIs must end with ')'";bwipp_raiseerror()}var _Pn=$k[--$j];$j--;var _Pq=$k[--$j];$k[$j++]=_Pn;$X(_Pq,"(");var _Pr=$k[--$j];if(_Pr){var _Ps=$k[--$j];$j--;var _Pv=$k[--$j];$k[$j++]=_Ps;$k[$j++]=_Pv}else{$k[$j++]=""}}var _Pw=$m();$k[$j++]=_Pw;if(_Pw>0){var _Px=$k[--$j];for(var _Pz=_Px,_Py=~~(_Px/2)+1;_Pz>=_Py;_Pz-=1){$r(_Pz,-1)}}else{$j--}$as($a(~~($m()/2)));var _Q2=$k[--$j];$_.ais=_Q2;$as($a($m()));var _Q6=$k[--$j];$k[$j++]=Infinity;$F(_Q6,function(){$k[$j++]=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);bwipp_parseinput()});var _Q9=$a();$_.vals=_Q9;$j--}else{var _QA=$k[--$j];$ax(_QA,"http://");var _QB=$k[--$j];if(!_QB){var _QC=$k[--$j];$ax(_QC,"HTTP://");var _QD=$k[--$j];if(!_QD){var _QE=$k[--$j];$ax(_QE,"https://");var _QF=$k[--$j];if(!_QF){var _QG=$k[--$j];$ax(_QG,"HTTPS://");var _QH=$k[--$j];if(!_QH){$k[$j-1]="bwipp.GS1dlURIbadScheme#2945";$k[$j++]="Scheme must be http:// HTTP:// https:// or HTTPS://";bwipp_raiseerror()}}}}$j--;var _QI=$k[--$j];$X(_QI,"/");var _QJ=$k[--$j];if(!_QJ){$k[$j-1]="bwipp.GS1dlMissingDomainOrPathInfo#2951";$k[$j++]="URI must contain a domain and path info";bwipp_raiseerror()}$j-=2;var _QK=$k[--$j];$X(_QK,"#");var _QL=$k[--$j];if(_QL){var _QM=$k[--$j];$j--;$k[$j-1]=_QM}var _QQ=$k[--$j];$X(_QQ,"?");var _QR=$k[--$j];if(!_QR){var _QS=$k[--$j];$k[$j++]="";$k[$j++]=null;$k[$j++]=_QS}var _QT=$k[--$j];$_.pp=_QT;$j--;var _QU=$k[--$j];$_.qp=_QU;$k[$j++]=Infinity;$k[$j++]=0;$k[$j++]=0;$k[$j++]=$_.pp;for(;;){var _QW=$k[--$j];$X(_QW,"/");var _QX=$k[--$j];if(!_QX){$j--;break}var _QY=$k[--$j];$j--;var _Qa=_QY.length+1;var _Qb=$k[--$j];var _Qc=$k[$j-1];$k[$j++]=$f(_Qa+_Qc);$k[$j++]=_Qb}$as($a($m()-1));var _Qf=$k[--$j];$_.pipos=_Qf;$j-=2;if($_.pipos.length<=1){$k[$j++]="bwipp.GS1dlNoAIinfo#2976";$k[$j++]="The path was too short to contain AI info";bwipp_raiseerror()}$k[$j++]=false;for(var _Qi=$_.pipos.length-2;_Qi>=0;_Qi-=2){var _Qk=$g($_.pipos,_Qi);var _Qo=$G($_.pp,_Qk,$f($g($_.pipos,_Qi+1)-_Qk)-1);var _Qq=$has($_.gs1syntax,_Qo);$k[$j++]=_Qi;$k[$j++]=_Qo;if(_Qq){var _Qs=$k[--$j];if($has($g($_.gs1syntax,_Qs),"dlpkey")){var _Qv=$k[--$j];$k[$j-1]=_Qv;$k[$j++]=true;break}}else{$j--}$j--}var _Qx=$k[--$j];if(!_Qx){$k[$j++]="bwipp.GS1dlNoAIinfo#2995";$k[$j++]="The path does not contain a valid primary key";bwipp_raiseerror()}var _Qz=$k[--$j];var _R0=$g($_.pipos,_Qz);$_.pp=$G($_.pp,_R0,$f($_.pp.length-_R0));$_.uriunescape=function(){var _R4=$k[--$j];$_.qq=_R4;var _R5=$k[--$j];$_.in=_R5;$_.out=$s($_.in.length);$_.badhex=false;$k[$j++]=0;$k[$j++]=0;for(;;){var _R9=$k[$j-2];if(_R9>=$_.in.length){break}var _RD=$k[$j-2];var _RE=$g($_.in,_RD);$k[$j++]=_RE;if(_RE==43&&$_.qq){$j--;var _RH=$k[--$j];$p($_.out,_RH,32);$k[$j-1]+=1;$k[$j++]=_RH+1}else{var _RJ=$k[$j-1];var _RL=$k[$j-3];if(_RJ==37&&_RL<$_.in.length-2){$j--;var _RP=$k[$j-2];var _RQ=$G($_.in,_RP+1,2);$k[$j++]=0;for(var _RR=0,_RS=_RQ.length;_RR<_RS;_RR++){var _RT=$g(_RQ,_RR);$k[$j++]=_RT;if(_RT>=48&&_RT<=57){$k[$j-1]-=48}else{var _RV=$k[$j-1];if(_RV>=65&&_RV<=70){$k[$j-1]-=55}else{var _RX=$k[$j-1];if(_RX>=97&&_RX<=102){$k[$j-1]-=87}else{$j-=2;$_.badhex=true;$k[$j++]=0;break}}}var _RZ=$k[--$j];var _Ra=$k[--$j];$k[$j++]=$f(_RZ+_Ra*16)}if($_.badhex){$j-=2;break}var _Rd=$k[--$j];var _Re=$k[--$j];$p($_.out,_Re,_Rd);$k[$j-1]+=3;$k[$j++]=_Re+1}else{var _Rh=$k[--$j];var _Ri=$k[--$j];$p($_.out,_Ri,_Rh);$k[$j-1]+=1;$k[$j++]=_Ri+1}}}if($_.badhex){$l();$k[$j++]="bwipp.GS1dlBadHexCharacter#3030";$k[$j++]="Invalid hex character";bwipp_raiseerror()}var _Rm=$k[--$j];$_.out=$G($_.out,0,_Rm);$k[$j-1]=$_.out};$_.isvaliddlpkeyseq=function(){var _Rp=$k[--$j];$_.in=_Rp;var _Rt=$g($_.gs1syntax,$g($_.in,0));$k[$j++]=false;$F($g(_Rt,"dlpkey"),function(){var _Rv=$k[--$j];$_.seq=_Rv;$_.i=1;$_.j=0;for(;;){if($_.i>=$_.in.length){break}if($_.j>=$_.seq.length){break}if($eq($g($_.in,$_.i),$g($_.seq,$_.j))){$_.i=$_.i+1}$_.j=$_.j+1}if($_.i==$_.in.length){$k[$j-1]=true;return true}})};$_.ais=$a(99);$_.vals=$a(99);$k[$j++]=Infinity;$k[$j++]=0;$k[$j++]=$_.pp;for(;;){var _SD=$k[--$j];$X(_SD,"/");var _SE=$k[--$j];if(_SE){var _SG=$k[--$j];$j--;var _SI=$k[--$j];var _SJ=$k[$j-1];$p($_.ais,_SJ,_SG);$X(_SI,"/");var _SK=$k[--$j];if(_SK){var _SL=$k[--$j];$k[$j-1]=_SL}else{var _SN=$k[--$j];$k[$j++]="";$k[$j++]=_SN}var _SP=$k[--$j];var _SR=$k[$j-2];$k[$j++]=$_.vals;$k[$j++]=_SR;$k[$j++]=_SP;$k[$j++]=false;$_.uriunescape();var _SS=$k[--$j];var _ST=$k[--$j];var _SU=$k[--$j];$p(_SU,_ST,_SS)}else{$j--;break}var _SV=$k[--$j];$k[$j-1]+=1;$k[$j++]=_SV}var _SX=$k[--$j];$_.plen=_SX;$k[$j++]=_SX;if(_SX>1){$k[$j++]=$G($_.ais,0,$_.plen);$_.isvaliddlpkeyseq();var _Sb=$k[--$j];if(!_Sb){$l();$k[$j++]="bwipp.GS1dlBadPathInfo#3076";$k[$j++]="The AIs in the path are not a valid key-qualifier sequence for the key";bwipp_raiseerror()}}$k[$j++]=$_.qp;for(;;){var _Sd=$k[$j-1];if(_Sd.length==0){$j--;break}var _Se=$k[--$j];$X(_Se,"&");var _Sf=$k[--$j];if(_Sf){var _Sg=$k[--$j];$k[$j-1]=_Sg}else{var _Si=$k[--$j];$k[$j++]="";$k[$j++]=_Si}var _Sj=$k[--$j];$X(_Sj,"=");var _Sk=$k[--$j];if(_Sk){var _Sl=$k[$j-1];var _So=true;$F(_Sl,function(){var _Sm=$k[--$j];_So=_So&&(_Sm>=48&&_Sm<=57)});if(_So){var _Sq=$k[--$j];$j--;var _Ss=$k[--$j];var _Su=$k[$j-2];$p($_.ais,_Su,_Sq);$k[$j++]=$_.vals;$k[$j++]=_Su;$k[$j++]=_Ss;$k[$j++]=true;$_.uriunescape();var _Sw=$k[--$j];var _Sx=$k[--$j];var _Sy=$k[--$j];$p(_Sy,_Sx,_Sw);var _Sz=$k[--$j];$k[$j-1]+=1;$k[$j++]=_Sz}else{$j-=3}}else{$j--}}var _T3=$k[$j-1];var _T5=$G($_.ais,$_.plen,$f(_T3-$_.plen));for(var _T6=0,_T7=_T5.length;_T6<_T7;_T6++){var _T8=$g(_T5,_T6);var _TA=$has($_.gs1syntax,_T8);$k[$j++]=_T8;if(_TA){var _TC=$k[$j-1];var _TD=$g($_.gs1syntax,_TC);var _TE=$has(_TD,"dlattr");$k[$j++]=_TD;$k[$j++]="dlattr";if(_TE){var _TF=$k[--$j];var _TG=$k[--$j];$k[$j++]=$g(_TG,_TF)}else{$j-=2;$k[$j++]=true}}else{$k[$j++]=$_.dontlint}var _TJ=$k[--$j];if(!_TJ){var _TK=$k[$j-1];var _TL=_TK.length;var _TM=$s(_TL+46);$P(_TM,0,"AI (");$P(_TM,4,_TK);$P(_TM,_TL+4,") is not a valid GS1 DL URI data attribute");$j--;var _TQ=$m()+2;$k[$j++]="bwipp.GS1dlInvalidDataAttribute#3110";$k[$j++]=_TM;$r(_TQ+1,2);$l();bwipp_raiseerror()}$j--}var _TT=$k[$j-1];var _TV=$G($_.ais,$_.plen,$f(_TT-$_.plen));for(var _TW=0,_TX=_TV.length;_TW<_TX;_TW++){$k[$j++]=$g(_TV,_TW);for(var _Ta=$_.plen;_Ta>=1;_Ta-=1){var _Tb=$k[$j-1];$k[$j++]=_Ta;$k[$j++]=_Tb;$k[$j++]=Infinity;$q($G($_.ais,0,$_.plen));$r($m()+3,-2);var _Tg=$k[--$j];var _Th=$k[--$j];$k[$j++]=_Tg;$r(_Th,1);var _Ti=$a();$k[$j++]=_Ti;$_.isvaliddlpkeyseq();var _Tj=$k[--$j];if(_Tj){var _Tk=$k[--$j];$k[$j-1]=_Tk;var _Tm=$k[$j-1];var _Tn=_Tm.length;var _To=$s(_Tn+50);$P(_To,0,"AI (");$P(_To,4,_Tm);$P(_To,_Tn+4,") from query params should be in the path info");$j--;var _Ts=$m()+2;$k[$j++]="bwipp.GS1dlAttributeMustBeQualifier#3128";$k[$j++]=_To;$r(_Ts+1,2);$l();bwipp_raiseerror()}}$j--}var _Tu=$k[--$j];$_.ais=$G($_.ais,0,_Tu);$_.vals=$G($_.vals,0,_Tu);$j--;for(var _U0=0,_Tz=$_.ais.length-1;_U0<=_Tz;_U0+=1){$k[$j++]=_U0;if($eq($g($_.ais,_U0),"01")){var _U3=$k[$j-1];var _U5=$g($_.vals,_U3);var _U6=_U5.length;$k[$j++]=_U5;if(_U6==8||(_U6==12||_U6==13)){var _U8=$Z($s(14),"00000000000000");$k[$j++]=_U8;$k[$j++]=_U8;var _U9=$k[$j-3];var _UA=$k[$j-1];$P(_UA,14-_U9.length,_U9);var _UC=$k[$j-2];$j-=3;var _UD=$k[$j-1];$p($_.vals,_UD,_UC)}else{$j--}}$j--}}$k[$j++]=Infinity;var _UE=$_.ais;for(var _UF=0,_UG=_UE.length;_UF<_UG;_UF++){var _UH=$g(_UE,_UF);$k[$j++]=true;$k[$j++]=_UH;if(_UH.length>=2){var _UI=$k[--$j];if($has($_.aifixed,$G(_UI,0,2))){$k[$j-1]=false}}else{$j--}}$_.fncs=$a();$_.lintnumeric=function(){var _UM=$k[--$j];$k[$j++]=true;$F(_UM,function(){var _UN=$k[--$j];if(_UN<48||_UN>57){$k[$j-1]=false;return true}});var _UO=$k[--$j];if(!_UO){$k[$j-1]="bwipp.GS1notNumeric#3170";$k[$j++]="Not numeric";$k[$j++]=false;return true}};$_.lintcset82=function(){var _UP=$k[--$j];$k[$j++]=true;$F(_UP,function(){var _UR=$k[--$j];if(!$has($_.cset82,_UR)){$k[$j-1]=false;return true}});var _UT=$k[--$j];if(!_UT){$k[$j-1]="bwipp.GS1badCSET82character#3175";$k[$j++]="Invalid CSET 82 character";$k[$j++]=false;return true}};$_.lintcset39=function(){var _UU=$k[--$j];$k[$j++]=true;$F(_UU,function(){var _UW=$k[--$j];if(!$has($_.cset39,_UW)){$k[$j-1]=false;return true}});var _UY=$k[--$j];if(!_UY){$k[$j-1]="bwipp.GS1badCSET39character#3180";$k[$j++]="Invalid CSET 39 character";$k[$j++]=false;return true}};$_.lintcset64=function(){var _UZ=$k[$j-1];$X(_UZ,"=");var _Ua=$k[--$j];if(_Ua){var _Ub=$k[--$j];$k[$j-1]=_Ub.length%3;$k[$j++]=_Ub;var _Ue=$k[$j-2];var _Uf=$k[$j-3];$k[$j-3]=$k[$j-1];$j-=2;$k[$j++]=_Uf;$k[$j++]=_Ue;$k[$j++]=false;if(_Ue==1&&$eq(_Uf,"=")){$k[$j-1]=true}var _Uh=$k[$j-2];var _Ui=$k[$j-3];if(_Uh==2&&_Ui.length==0){$k[$j-1]=true}var _Uj=$k[--$j];if(!_Uj){$j-=5;$k[$j++]="bwipp.GS1badCSET64padding#3189";$k[$j++]="Invalid CSET 64 padding";$k[$j++]=false;return true}$j-=2;var _Uk=$k[--$j];var _Ul=$k[--$j];$k[$j++]=_Uk;$k[$j++]=_Ul}$j--;var _Um=$k[--$j];$k[$j++]=true;$F(_Um,function(){var _Uo=$k[--$j];if(!$has($_.cset64,_Uo)){$k[$j-1]=false;return true}});var _Uq=$k[--$j];if(!_Uq){$k[$j-1]="bwipp.GS1badCSET64character#3195";$k[$j++]="Invalid CSET 64 character";$k[$j++]=false;return true}};$_.lintgcppos1=function(){var _Ur=$k[$j-1];if(_Ur.length<2){$j-=2;$k[$j++]="bwipp.GS1gcpTooShort#3199";$k[$j++]="Value is too short to contain a GS1 Company Prefix";$k[$j++]=false;return true}var _Us=$k[--$j];var _Ut=$g(_Us,0);var _Uu=$g(_Us,1);if(_Ut<48||_Ut>57||(_Uu<48||_Uu>57)){$k[$j-1]="bwipp.GS1badGCP#3202";$k[$j++]="Non-numeric GS1 Company Prefix";$k[$j++]=false;return true}};$_.lintgcppos2=function(){var _Uv=$k[$j-1];if(_Uv.length<1){$j-=2;$k[$j++]="bwipp.GS1valueTooShortForOffsetGCP#3207";$k[$j++]="Value is too short to contain an offset GS1 Company Prefix";$k[$j++]=false;return true}var _Uw=$k[--$j];$k[$j++]=$G(_Uw,1,_Uw.length-1);$_.lintgcppos1()};$_.lintimporteridx=function(){var _Uy=$k[--$j];$X("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",_Uy);var _Uz=$k[--$j];if(!_Uz){$j-=2;$k[$j++]="bwipp.GS1badImporterIndex#3213";$k[$j++]="Invalid importer index";$k[$j++]=false;return true}$j-=3};$_.lintcsum=function(){var _V1=$k[--$j];var _V2=_V1.length%2==0?3:1;$k[$j++]=Infinity;$k[$j++]=_V2;$F(_V1,function(){var _V3=$k[--$j];var _V4=$k[--$j];$k[$j++]=(_V3-48)*_V4;$k[$j++]=4-_V4});$j--;var _V5=$m()+1;$k[$j++]=0;for(var _V6=0,_V7=_V5-1;_V6<_V7;_V6++){var _V8=$k[--$j];var _V9=$k[--$j];$k[$j++]=$f(_V9+_V8)}var _VA=$k[--$j];$j--;if(_VA%10!=0){$k[$j-1]="bwipp.GS1badChecksum#3222";$k[$j++]="Bad checksum";$k[$j++]=false;return true}};$_.lintcsumalpha=function(){var _VD=$k[$j-1];if(_VD.length<2){$j-=2;$k[$j++]="bwipp.GS1alphaTooShort#3226";$k[$j++]="Alphanumeric string is too short to check";$k[$j++]=false;return true}var _VE=$k[$j-1];$k[$j++]=_VE.length-2;var _VH=$G(_VE,0,_VE.length-2);$k[$j++]=Infinity;$F(_VH,function(){var _VI=$k[$j-1];var _VK=$has($_.cset82,_VI);if(_VK){var _VM=$k[--$j];$k[$j++]=$g($_.cset82,_VM)}else{$k[$j++]=-1;return true}});var _VO=$k[$j-1];if(_VO==-1){$l();$j-=3;$k[$j++]="bwipp.GS1UnknownCSET82Character#3232";$k[$j++]="Unknown CSET 82 character";$k[$j++]=false;return true}$as($a($m()));var _VR=$k[--$j];$k[$j-1]=_VR;var _VT=$k[$j-2];var _VU=$k[$j-3];$k[$j-3]=$k[$j-1];$j-=2;var _VX=$G(_VU,_VT,2);$k[$j++]=Infinity;$F(_VX,function(){var _VY=$k[$j-1];var _Va=$has($_.cset32,_VY);if(_Va){var _Vc=$k[--$j];$k[$j++]=$g($_.cset32,_Vc)}else{$k[$j++]=-1;return true}});var _Ve=$k[$j-1];if(_Ve==-1){$l();$j-=2;$k[$j++]="bwipp.GS1UnknownCSET32Character#3238";$k[$j++]="Unknown CSET 32 character";$k[$j++]=false;return true}$as($a($m()));var _Vh=$k[--$j];$j--;var _Vm=$k[--$j];var _Vn=_Vm.length;$k[$j++]=$f($g(_Vh,0)*32+$g(_Vh,1));$k[$j++]=_Vm;$k[$j++]=$_.gs1process_primes;$k[$j++]=_Vn;if(_Vn>$_.gs1process_primes.length){$j-=5;$k[$j++]="bwipp.GS1alphaTooLong#3243";$k[$j++]="Alphanumeric string is too long to check";$k[$j++]=false;return true}var _Vo=$k[--$j];var _Vp=$k[--$j];var _Vq=$G(_Vp,0,_Vo);for(var _Vr=0,_Vs=_Vq.length;_Vr<_Vs;_Vr++){var _Vu=$k[--$j];$k[$j++]=$g(_Vq,_Vr);$k[$j++]=_Vu}var _Vv=$k[--$j];$k[$j++]=0;$F(_Vv,function(){var _Vw=$k[$j-3];var _Vx=$k[$j-1];var _Vy=$k[$j-2];$j-=3;$k[$j++]=$f(_Vy+_Vw*_Vx)});var _Vz=$k[--$j];var _W0=$k[--$j];if(_W0!=_Vz%1021){$k[$j-1]="bwipp.GS1badAlphaCheckCharacters#3246";$k[$j++]="Bad alphanumeric check characters";$k[$j++]=false;return true}};$_.lintiso3166=function(){var _W2=$k[--$j];if(!$has($_.iso3166,_W2)){$k[$j-1]="bwipp.GS1UnknownCountry#3250";$k[$j++]="Unknown country code";$k[$j++]=false;return true}};$_.lintiso3166999=function(){var _W4=$k[$j-1];if($ne(_W4,"999")){var _W6=$k[--$j];if(!$has($_.iso3166,_W6)){$k[$j-1]="bwipp.GS1UnknownCountryOr999#3255";$k[$j++]="Unknown country code or not 999";$k[$j++]=false;return true}}else{$j--}};$_.lintiso3166alpha2=function(){var _W9=$k[--$j];if(!$has($_.iso3166alpha2,_W9)){$k[$j-1]="bwipp.GS1UnknownCountryAlpha#3262";$k[$j++]="Unknown country alpha code";$k[$j++]=false;return true}};$_.lintiso4217=function(){var _WC=$k[--$j];if(!$has($_.iso4217,_WC)){$k[$j-1]="bwipp.GS1UnknownCurrency#3266";$k[$j++]="Unknown currency code";$k[$j++]=false;return true}};$_.lintiso5218=function(){var _WE=$k[--$j];if($ne(_WE,"0")&&($ne(_WE,"1")&&($ne(_WE,"2")&&$ne(_WE,"9")))){$k[$j-1]="bwipp.GS1biologicalSexCode#3271";$k[$j++]="Invalid biological sex code";$k[$j++]=false;return true}};$_.lintiban=function(){var _WF=$k[$j-1];if(_WF.length<=4){$j-=2;$k[$j++]="bwipp.GS1tooShort#3276";$k[$j++]="IBAN too short";$k[$j++]=false;return true}var _WG=$k[$j-1];$k[$j++]=true;$F(_WG,function(){var _WH=$s(1);$k[$j++]=_WH;$k[$j++]=_WH;$k[$j++]=0;$r(4,-1);var _WI=$k[--$j];var _WJ=$k[--$j];var _WK=$k[--$j];$p(_WK,_WJ,_WI);var _WL=$k[--$j];$X("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",_WL);var _WM=$k[--$j];if(!_WM){$j-=2;$k[$j++]=false;return true}$j-=3});var _WN=$k[--$j];if(!_WN){$j-=2;$k[$j++]="bwipp.GS1badIBANcharacter#3283";$k[$j++]="Invalid IBAN character";$k[$j++]=false;return true}var _WO=$k[$j-1];$k[$j++]=Infinity;$k[$j++]=Infinity;$F(_WO);$r($m(),-4);$as($a($m()));var _WW=$k[--$j];$j--;$F(_WW,function(){var _WZ=$k[--$j];var _Wa=_WZ-48;$k[$j++]=_Wa;if(_Wa>9){var _Wb=$k[--$j];var _Wc=_Wb-7;$k[$j++]=~~(_Wc/10);$k[$j++]=_Wc%10}});$as($a($m()));var _Wf=$k[--$j];$j--;var _Wk=0;$F(_Wf,function(){var _Wi=$k[--$j];_Wk=$f(_Wi+_Wk*10)%97});if(_Wk!=1){$j-=2;$k[$j++]="bwipp.GS1badIBANchecksum#3288";$k[$j++]="IBAN checksum incorrect";$k[$j++]=false;return true}var _Wl=$k[--$j];$k[$j++]=$G(_Wl,0,2);$_.lintiso3166alpha2()};$_.lintzero=function(){var _Wn=$k[--$j];if($ne(_Wn,"0")){$k[$j-1]="bwipp.GS1zeroRequired#3293";$k[$j++]="Zero is required";$k[$j++]=false;return true}};$_.lintnonzero=function(){var _Wo=$k[--$j];$k[$j++]=false;$F(_Wo,function(){var _Wp=$k[--$j];if(_Wp!=48){$k[$j-1]=true}});var _Wq=$k[--$j];if(!_Wq){$k[$j-1]="bwipp.GS1zeroNotPermitted#3298";$k[$j++]="Zero not permitted";$k[$j++]=false;return true}};$_.lintnozeroprefix=function(){var _Wr=$k[--$j];if(_Wr.length>1&&$g(_Wr,0)==48){$k[$j-1]="bwipp.GS1badZeroPrefix#3303";$k[$j++]="Zero prefix is not permitted";$k[$j++]=false;return true}};$_.linthasnondigit=function(){var _Wt=$k[--$j];$k[$j++]=false;$F(_Wt,function(){var _Wu=$k[--$j];if(_Wu<48||_Wu>57){$k[$j-1]=true;return true}});var _Wv=$k[--$j];if(!_Wv){$k[$j-1]="bwipp.GS1requiresNonDigit#3309";$k[$j++]="A non-digit character is required";$k[$j++]=false;return true}};$_.linthyphen=function(){var _Ww=$k[--$j];if($ne(_Ww,"-")){$k[$j-1]="bwipp.GS1hyphenRequired#3313";$k[$j++]="A hyphen is required";$k[$j++]=false;return true}};$_.lintyyyymmd0=function(){var _Wx=$k[$j-1];var _Wz=$i($G(_Wx,4,2));$k[$j++]=_Wz;if(_Wz<1||_Wz>12){$j-=3;$k[$j++]="bwipp.GS1badMonth#3317";$k[$j++]="Invalid month";$k[$j++]=false;return true}var _X0=$k[--$j];if(_X0==2){var _X1=$k[$j-1];var _X3=$i($G(_X1,0,4));$k[$j++]=_X3%400==0||_X3%4==0&&_X3%100!=0?29:28}else{var _X5=$k[$j-1];$k[$j++]=$g($_.gs1process_daysinmonth,$i($G(_X5,4,2))-1)}var _X8=$k[--$j];var _X9=$k[--$j];if(_X8<$i($G(_X9,6,2))){$k[$j-1]="bwipp.GS1badDay#3323";$k[$j++]="Invalid day of month";$k[$j++]=false;return true}};$_.lintyyyymmdd=function(){var _XB=$k[$j-1];if(_XB.length!=8){$j-=2;$k[$j++]="bwipp.GS1badDateLength#3327";$k[$j++]="Invalid length for yyyymmdd date";$k[$j++]=false;return true}var _XC=$k[$j-1];if($i($G(_XC,6,2))<1){$j-=2;$k[$j++]="bwipp.GS1badDay#3328";$k[$j++]="Invalid day of month";$k[$j++]=false;return true}$_.lintyyyymmd0()};$_.lintyymmd0=function(){var _XE=$k[$j-1];var _XG=$i($G(_XE,0,2));var _XH=_XG-21;$k[$j++]=_XG;$k[$j++]=_XH;if(_XH>=51){$k[$j-1]="19"}else{var _XI=$k[--$j];if(_XI<=-50){$k[$j++]="21"}else{$k[$j++]="20"}}var _XJ=$k[--$j];$k[$j-1]=_XJ;var _XL=$s(8);$k[$j++]=_XL;$k[$j++]=_XL;$k[$j++]=0;$r(4,-1);var _XM=$k[--$j];var _XN=$k[--$j];var _XO=$k[--$j];$P(_XO,_XN,_XM);var _XP=$k[$j-1];$k[$j++]=_XP;$k[$j++]=2;$r(4,-1);var _XQ=$k[--$j];var _XR=$k[--$j];var _XS=$k[--$j];$P(_XS,_XR,_XQ);$_.lintyyyymmd0()};$_.lintyymmdd=function(){var _XT=$k[$j-1];if(_XT.length!=6){$j-=2;$k[$j++]="bwipp.GS1badDateLength#3340";$k[$j++]="Invalid length for yymmdd date";$k[$j++]=false;return true}var _XU=$k[$j-1];if($i($G(_XU,4,2))<1){$j-=2;$k[$j++]="bwipp.GS1badDay#3341";$k[$j++]="Invalid day of month";$k[$j++]=false;return true}$_.lintyymmd0()};$_.linthh=function(){var _XW=$k[--$j];if($i($G(_XW,0,2))>23){$k[$j-1]="bwipp.GS1badHour#3346";$k[$j++]="Invalid hour of day";$k[$j++]=false;return true}};$_.lintmi=function(){var _XY=$k[--$j];if($i($G(_XY,0,2))>59){$k[$j-1]="bwipp.GS1badMinute#3350";$k[$j++]="Invalid minute in the hour";$k[$j++]=false;return true}};$_.lintss=function(){var _Xa=$k[--$j];if($i($G(_Xa,0,2))>59){$k[$j-1]="bwipp.GS1badSecond#3354";$k[$j++]="Invalid second in the minute";$k[$j++]=false;return true}};$_.linthhmi=function(){var _Xc=$k[$j-1];if($i($G(_Xc,0,2))>23){$j-=2;$k[$j++]="bwipp.GS1badHour#3358";$k[$j++]="Invalid hour of day";$k[$j++]=false;return true}var _Xe=$k[--$j];if($i($G(_Xe,2,2))>59){$k[$j-1]="bwipp.GS1badMinute#3359";$k[$j++]="Invalid minute in the hour";$k[$j++]=false;return true}};$_.lintmmoptss=function(){var _Xg=$k[$j-1];var _Xh=_Xg.length;if(_Xh!=2&&_Xh!=4){$k[$j-1]="bwipp.GS1badTimeLength#3364";$k[$j++]="Invalid length for optional minutes and seconds";$k[$j++]=false;return true}var _Xi=$k[$j-1];if($i($G(_Xi,0,2))>59){$j-=2;$k[$j++]="bwipp.GS1badMinute#3366";$k[$j++]="Invalid minute in the hour";$k[$j++]=false;return true}var _Xk=$k[$j-1];if(_Xk.length>=4){var _Xl=$k[$j-1];if($i($G(_Xl,2,2))>59){$j-=2;$k[$j++]="bwipp.GS1badSecond#3368";$k[$j++]="Invalid second in the minute";$k[$j++]=false;return true}}$j--};$_.lintyesno=function(){var _Xn=$k[--$j];if($ne(_Xn,"0")&&$ne(_Xn,"1")){$k[$j-1]="bwipp.GS1badBoolean#3375";$k[$j++]="Neither 0 nor 1 for yes or no";$k[$j++]=false;return true}};$_.lintwinding=function(){var _Xo=$k[--$j];if($ne(_Xo,"0")&&($ne(_Xo,"1")&&$ne(_Xo,"9"))){$k[$j-1]="bwipp.GS1badWinding#3381";$k[$j++]="Invalid winding direction";$k[$j++]=false;return true}};$_.lintpieceoftotal=function(){var _Xp=$k[$j-1];if(_Xp.length%2!=0){$j-=2;$k[$j++]="bwipp.GS1badPieceTotalLength#3386";$k[$j++]="Invalid piece/total length";$k[$j++]=false;return true}var _Xq=$k[$j-1];var _Xs=$i($G(_Xq,0,~~(_Xq.length/2)));$k[$j++]=_Xs;if(_Xs==0){$j-=3;$k[$j++]="bwipp.GS1badPieceNumber#3388";$k[$j++]="Invalid piece number";$k[$j++]=false;return true}var _Xt=$k[--$j];var _Xu=$k[--$j];var _Xv=~~(_Xu.length/2);var _Xx=$i($G(_Xu,_Xv,_Xv));$k[$j++]=_Xt;$k[$j++]=_Xx;if(_Xx==0){$j-=3;$k[$j++]="bwipp.GS1badPieceTotal#3390";$k[$j++]="Invalid total number";$k[$j++]=false;return true}var _Xy=$k[--$j];var _Xz=$k[--$j];if($gt(_Xz,_Xy)){$k[$j-1]="bwipp.GS1pieceExceedsTotal#3391";$k[$j++]="Piece number exceeds total";$k[$j++]=false;return true}};$_.lintposinseqslash=function(){var _Y0=$k[--$j];$X(_Y0,"/");var _Y1=$k[--$j];if(!_Y1){$j-=2;$k[$j++]="bwipp.invalidPosInSeqFormat#3396";$k[$j++]="Invalid / format";$k[$j++]=false;return true}var _Y2=$k[--$j];$k[$j-1]=_Y2;var _Y4=$k[$j-1];if(_Y4.length==0){$j-=3;$k[$j++]="bwipp.invalidPosInSeqFormat#3400";$k[$j++]="Invalid / format";$k[$j++]=false;return true}var _Y6=$k[$j-2];if(_Y6.length==0){$j-=3;$k[$j++]="bwipp.invalidPosInSeqFormat#3403";$k[$j++]="Invalid / format";$k[$j++]=false;return true}var _Y7=$k[$j-1];$k[$j++]=true;$F(_Y7,function(){var _Y8=$k[--$j];if(_Y8<48||_Y8>57){$k[$j-1]=false;return true}});var _Y9=$k[--$j];if(!_Y9){$j-=3;$k[$j++]="bwipp.positionNotNumeric#3408";$k[$j++]="Position is not numeric";$k[$j++]=false;return true}var _YB=$k[$j-2];$k[$j++]=true;$F(_YB,function(){var _YC=$k[--$j];if(_YC<48||_YC>57){$k[$j-1]=false;return true}});var _YD=$k[--$j];if(!_YD){$j-=3;$k[$j++]="bwipp.endNotNumeric#3412";$k[$j++]="End is not numeric";$k[$j++]=false;return true}var _YE=$k[$j-1];if($g(_YE,0)==48){$j-=3;$k[$j++]="bwipp.positionZeroPrefix#3413";$k[$j++]="Position cannot have zero prefix";$k[$j++]=false;return true}var _YH=$k[$j-2];if($g(_YH,0)==48){$j-=3;$k[$j++]="bwipp.endZeroPrefix#3414";$k[$j++]="End cannot have zero prefix";$k[$j++]=false;return true}var _YJ=$k[--$j];var _YK=$k[--$j];if($i(_YK)<$i(_YJ)){$k[$j-1]="bwipp.positionExceedsEnd#3416";$k[$j++]="Position exceeds end";$k[$j++]=false;return true}};$_.lintpcenc=function(){for(;;){var _YL=$k[--$j];$X(_YL,"%");var _YM=$k[--$j];if(!_YM){$j--;break}$j-=2;var _YN=$k[$j-1];if(_YN.length<2){$j-=2;$k[$j++]="bwipp.GS1badPercentEscape#3422";$k[$j++]="Invalid % escape";$k[$j++]=false;break}var _YO=$k[$j-1];var _YP=$G(_YO,0,2);$k[$j++]=true;for(var _YQ=0,_YR=_YP.length;_YQ<_YR;_YQ++){var _YT=$s(1);$p(_YT,0,$g(_YP,_YQ));$X("0123456789ABCDEFabcdef",_YT);var _YU=$k[--$j];if(!_YU){$j-=2;$k[$j++]=false;break}$j-=3}var _YV=$k[--$j];if(!_YV){$j-=2;$k[$j++]="bwipp.GS1badPercentChars#3429";$k[$j++]="Invalid characters for percent encoding";$k[$j++]=false;break}}};$_.lintcouponcode=function(){var _YW=$k[$j-1];$k[$j++]=true;$F(_YW,function(){var _YX=$k[--$j];if(_YX<48||_YX>57){$k[$j-1]=false;return true}});var _YY=$k[--$j];if(!_YY){$j-=2;$k[$j++]="bwipp.GS1couponNotNumeric#3437";$k[$j++]="Coupon not numeric";$k[$j++]=false;return true}var _YZ=$k[$j-1];if(_YZ.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortGCPVLI#3441";$k[$j++]="Coupon too short: Missing GCP VLI";$k[$j++]=false;return true}var _Ya=$k[$j-1];var _Yc=$i($G(_Ya,0,1));$k[$j++]=_Yc;if(_Yc>6){$j-=3;$k[$j++]="bwipp.GS1couponBadGCPVLI#3444";$k[$j++]="Coupon GCP length indicator must be 0-6";$k[$j++]=false;return true}var _Yd=$k[--$j];var _Ye=$k[$j-1];$k[$j++]=_Yd+6+1;if(_Yd+6+1>_Ye.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortGCP#3448";$k[$j++]="Coupon too short: GCP truncated";$k[$j++]=false;return true}var _Yf=$k[--$j];var _Yg=$k[--$j];var _Yh=$G(_Yg,_Yf,$f(_Yg.length-_Yf));$k[$j++]=_Yh;if(_Yh.length<6){$j-=2;$k[$j++]="bwipp.GS1couponTooShortOfferCode#3454";$k[$j++]="Coupon too short: Offer Code truncated";$k[$j++]=false;return true}var _Yi=$k[--$j];var _Yj=$G(_Yi,6,_Yi.length-6);$k[$j++]=_Yj;if(_Yj.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortSaveValueVLI#3460";$k[$j++]="Coupon too short: Missing Save Value VLI";$k[$j++]=false;return true}var _Yk=$k[$j-1];var _Ym=$i($G(_Yk,0,1));$k[$j++]=_Ym;if(_Ym<1||_Ym>5){$j-=3;$k[$j++]="bwipp.GS1couponBadSaveValueVLI#3463";$k[$j++]="Coupon Save Value length indicator must be 1-5";$k[$j++]=false;return true}var _Yn=$k[--$j];var _Yo=$k[$j-1];$k[$j++]=_Yn+1;if(_Yn+1>_Yo.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortSaveValue#3467";$k[$j++]="Coupon too short: Save Value truncated";$k[$j++]=false;return true}var _Yp=$k[--$j];var _Yq=$k[--$j];var _Yr=$G(_Yq,_Yp,$f(_Yq.length-_Yp));$k[$j++]=_Yr;if(_Yr.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort1stPurchaseRequirementVLI#3473";$k[$j++]="Coupon too short: Missing 1st Purchase Requirement VLI";$k[$j++]=false;return true}var _Ys=$k[$j-1];var _Yu=$i($G(_Ys,0,1));$k[$j++]=_Yu;if(_Yu<1||_Yu>5){$j-=3;$k[$j++]="bwipp.GS1couponBad1stPurchaseRequirementVLI#3476";$k[$j++]="Coupon 1st Purchase Requirement length indicator must be 1-5";$k[$j++]=false;return true}var _Yv=$k[--$j];var _Yw=$k[$j-1];$k[$j++]=_Yv+1;if(_Yv+1>_Yw.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShort1stPurchaseRequirement#3480";$k[$j++]="Coupon too short: 1st Purchase Requirement truncated";$k[$j++]=false;return true}var _Yx=$k[--$j];var _Yy=$k[--$j];var _Yz=$G(_Yy,_Yx,$f(_Yy.length-_Yx));$k[$j++]=_Yz;if(_Yz.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort1stPurchaseRequirementCode#3486";$k[$j++]="Coupon too short: Missing 1st Purchase Requirement Code";$k[$j++]=false;return true}var _Z0=$k[$j-1];var _Z2=$i($G(_Z0,0,1));if(_Z2>4&&_Z2!=9){$j-=2;$k[$j++]="bwipp.GS1couponBad1stPurchaseRequirementCode#3489";$k[$j++]="Coupon 1st Purchase Requirement Code must be 0-4 or 9";$k[$j++]=false;return true}var _Z3=$k[--$j];var _Z4=$G(_Z3,1,_Z3.length-1);$k[$j++]=_Z4;if(_Z4.length<3){$j-=2;$k[$j++]="bwipp.GS1couponTooShort1stPurchaseFamilyCode#3495";$k[$j++]="Coupon too short: 1st Purchase Family Code truncated";$k[$j++]=false;return true}var _Z5=$k[--$j];var _Z6=$G(_Z5,3,_Z5.length-3);$k[$j++]=_Z6;if(_Z6.length>=1){var _Z7=$k[$j-1];if($i($G(_Z7,0,1))==1){var _Z9=$k[--$j];var _ZA=$G(_Z9,1,_Z9.length-1);$k[$j++]=_ZA;if(_ZA.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortAdditionalPurchaseRulesCode#3505";$k[$j++]="Coupon too short: Missing Additional Purchase Rules Code";$k[$j++]=false;return true}var _ZB=$k[$j-1];if($i($G(_ZB,0,1))>3){$j-=2;$k[$j++]="bwipp.GS1couponBadAdditionalPurchaseRulesCode#3508";$k[$j++]="Coupon Additional Purchase Rules Code must be 0-3";$k[$j++]=false;return true}var _ZD=$k[--$j];var _ZE=$G(_ZD,1,_ZD.length-1);$k[$j++]=_ZE;if(_ZE.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseRequirementVLI#3514";$k[$j++]="Coupon too short: Missing 2nd Purchase Requirement VLI";$k[$j++]=false;return true}var _ZF=$k[$j-1];var _ZH=$i($G(_ZF,0,1));$k[$j++]=_ZH;if(_ZH<1||_ZH>5){$j-=3;$k[$j++]="bwipp.GS1couponBad2ndPurchaseRequirementVLI#3517";$k[$j++]="Coupon 2nd Purchase Requirement length indicator must be 1-5";$k[$j++]=false;return true}var _ZI=$k[--$j];var _ZJ=$k[$j-1];$k[$j++]=_ZI+1;if(_ZI+1>_ZJ.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseRequirement#3521";$k[$j++]="Coupon too short: 2nd Purchase Requirement truncated";$k[$j++]=false;return true}var _ZK=$k[--$j];var _ZL=$k[--$j];var _ZM=$G(_ZL,_ZK,$f(_ZL.length-_ZK));$k[$j++]=_ZM;if(_ZM.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseRequirementCode#3527";$k[$j++]="Coupon too short: Missing 2nd Purchase Requirement Code";$k[$j++]=false;return true}var _ZN=$k[$j-1];var _ZP=$i($G(_ZN,0,1));if(_ZP>4&&_ZP!=9){$j-=2;$k[$j++]="bwipp.GS1couponBad2ndPurchaseRequirementCode#3530";$k[$j++]="Coupon 2nd Purchase Requirement Code must be 0-4 or 9";$k[$j++]=false;return true}var _ZQ=$k[--$j];var _ZR=$G(_ZQ,1,_ZQ.length-1);$k[$j++]=_ZR;if(_ZR.length<3){$j-=2;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseFamilyCode#3536";$k[$j++]="Coupon too short: 2nd Purchase Family Code truncated";$k[$j++]=false;return true}var _ZS=$k[--$j];var _ZT=$G(_ZS,3,_ZS.length-3);$k[$j++]=_ZT;if(_ZT.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseGCPVLI#3542";$k[$j++]="Coupon too short: Missing 2nd Purchase GCP VLI";$k[$j++]=false;return true}var _ZU=$k[$j-1];var _ZW=$i($G(_ZU,0,1));$k[$j++]=_ZW;if(_ZW>6&&_ZW!=9){$j-=3;$k[$j++]="bwipp.GS1couponBad2ndPurchaseGCPVLI#3545";$k[$j++]="Coupon 2nd Purchase GCP length indicator must be 0-6 or 9";$k[$j++]=false;return true}var _ZX=$k[$j-1];if(_ZX!=9){$k[$j-1]+=6}else{$k[$j-1]=0}var _ZZ=$k[--$j];var _Za=$k[$j-1];$k[$j++]=_ZZ+1;if(_ZZ+1>_Za.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShort2ndPurchaseGCP#3549";$k[$j++]="Coupon too short: 2nd Purchase GCP truncated";$k[$j++]=false;return true}var _Zb=$k[--$j];var _Zc=$k[--$j];$k[$j++]=$G(_Zc,_Zb,$f(_Zc.length-_Zb))}}var _Ze=$k[$j-1];if(_Ze.length>=1){var _Zf=$k[$j-1];if($i($G(_Zf,0,1))==2){var _Zh=$k[--$j];var _Zi=$G(_Zh,1,_Zh.length-1);$k[$j++]=_Zi;if(_Zi.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseRequirementVLI#3561";$k[$j++]="Coupon too short: Missing 3rd Purchase Requirement VLI";$k[$j++]=false;return true}var _Zj=$k[$j-1];var _Zl=$i($G(_Zj,0,1));$k[$j++]=_Zl;if(_Zl<1||_Zl>5){$j-=3;$k[$j++]="bwipp.GS1couponBad3rdPurchaseRequirementVLI#3564";$k[$j++]="Coupon 3rd Purchase Requirement length indicator must be 1-5";$k[$j++]=false;return true}var _Zm=$k[--$j];var _Zn=$k[$j-1];$k[$j++]=_Zm+1;if(_Zm+1>_Zn.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseRequirement#3568";$k[$j++]="Coupon too short: 3rd Purchase Requirement truncated";$k[$j++]=false;return true}var _Zo=$k[--$j];var _Zp=$k[--$j];var _Zq=$G(_Zp,_Zo,$f(_Zp.length-_Zo));$k[$j++]=_Zq;if(_Zq.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseRequirementCode#3574";$k[$j++]="Coupon too short: Missing 3rd Purchase Requirement Code";$k[$j++]=false;return true}var _Zr=$k[$j-1];var _Zt=$i($G(_Zr,0,1));if(_Zt>4&&_Zt!=9){$j-=2;$k[$j++]="bwipp.GS1couponBad3rdPurchaseRequirementCode#3577";$k[$j++]="Coupon 3rd Purchase Requirement Code must be 0-4 or 9";$k[$j++]=false;return true}var _Zu=$k[--$j];var _Zv=$G(_Zu,1,_Zu.length-1);$k[$j++]=_Zv;if(_Zv.length<3){$j-=2;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseFamilyCode#3583";$k[$j++]="Coupon too short: 3rd Purchase Family Code truncated";$k[$j++]=false;return true}var _Zw=$k[--$j];var _Zx=$G(_Zw,3,_Zw.length-3);$k[$j++]=_Zx;if(_Zx.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseGCPVLI#3589";$k[$j++]="Coupon too short: Missing 3rd Purchase GCP VLI";$k[$j++]=false;return true}var _Zy=$k[$j-1];var _a0=$i($G(_Zy,0,1));$k[$j++]=_a0;if(_a0>6&&_a0!=9){$j-=3;$k[$j++]="bwipp.GS1couponBad3rdPurchaseGCPVLI#3592";$k[$j++]="Coupon 3rd Purchase GCP length indicator must be 0-6 or 9";$k[$j++]=false;return true}var _a1=$k[$j-1];if(_a1!=9){$k[$j-1]+=6}else{$k[$j-1]=0}var _a3=$k[--$j];var _a4=$k[$j-1];$k[$j++]=_a3+1;if(_a3+1>_a4.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShort3rdPurchaseGCP#3596";$k[$j++]="Coupon too short: 3rd Purchase GCP truncated";$k[$j++]=false;return true}var _a5=$k[--$j];var _a6=$k[--$j];$k[$j++]=$G(_a6,_a5,$f(_a6.length-_a5))}}$_.couponexpire=-1;var _a8=$k[$j-1];if(_a8.length>=1){var _a9=$k[$j-1];if($i($G(_a9,0,1))==3){var _aB=$k[--$j];var _aC=$G(_aB,1,_aB.length-1);$k[$j++]=_aC;if(_aC.length<6){$j-=2;$k[$j++]="bwipp.GS1couponTooShortExpirationDate#3609";$k[$j++]="Coupon too short: Expiration date";$k[$j++]=false;return true}var _aD=$k[$j-1];var _aF=$i($G(_aD,2,2));$k[$j++]=_aF;if(_aF<1||_aF>12){$j-=3;$k[$j++]="bwipp.GS1couponExpirationDateBadMonth#3611";$k[$j++]="Invalid month in expiration date";$k[$j++]=false;return true}var _aG=$k[--$j];if(_aG==2){var _aH=$k[$j-1];var _aJ=$i($G(_aH,0,2));var _aK=_aJ-21;$k[$j++]=_aJ;$k[$j++]=_aK;if(_aK>=51){$j--;$k[$j-1]+=1900}else{var _aM=$k[--$j];if(_aM<=-50){$k[$j-1]+=2100}else{$k[$j-1]+=2e3}}var _aP=$k[--$j];$k[$j++]=_aP%400==0||_aP%4==0&&_aP%100!=0?29:28}else{var _aR=$k[$j-1];$k[$j++]=$g($_.gs1process_daysinmonth,$i($G(_aR,2,2))-1)}var _aU=$k[--$j];var _aV=$k[$j-1];var _aX=$i($G(_aV,4,2));if(_aU<_aX||_aX<1){$j-=2;$k[$j++]="bwipp.GS1couponExpirationDateBadDay#3620";$k[$j++]="Invalid day of month in expiration date";$k[$j++]=false;return true}var _aY=$k[--$j];$_.couponexpire=$i($G(_aY,0,6));$k[$j++]=$G(_aY,6,_aY.length-6)}}var _ab=$k[$j-1];if(_ab.length>=1){var _ac=$k[$j-1];if($i($G(_ac,0,1))==4){var _ae=$k[--$j];var _af=$G(_ae,1,_ae.length-1);$k[$j++]=_af;if(_af.length<6){$j-=2;$k[$j++]="bwipp.GS1couponTooShortStartDate#3632";$k[$j++]="Coupon too short: Start date";$k[$j++]=false;return true}var _ag=$k[$j-1];var _ai=$i($G(_ag,2,2));$k[$j++]=_ai;if(_ai<1||_ai>12){$j-=3;$k[$j++]="bwipp.GS1couponStartDateBadMonth#3634";$k[$j++]="Invalid month in start date";$k[$j++]=false;return true}var _aj=$k[--$j];if(_aj==2){var _ak=$k[$j-1];var _am=$i($G(_ak,0,2));var _an=_am-21;$k[$j++]=_am;$k[$j++]=_an;if(_an>=51){$j--;$k[$j-1]+=1900}else{var _ap=$k[--$j];if(_ap<=-50){$k[$j-1]+=2100}else{$k[$j-1]+=2e3}}var _as=$k[--$j];$k[$j++]=_as%400==0||_as%4==0&&_as%100!=0?29:28}else{var _au=$k[$j-1];$k[$j++]=$g($_.gs1process_daysinmonth,$i($G(_au,2,2))-1)}var _ax=$k[--$j];var _ay=$k[$j-1];var _b0=$i($G(_ay,4,2));if(_ax<_b0||_b0<1){$j-=2;$k[$j++]="bwipp.GS1couponStartDateBadDay#3643";$k[$j++]="Invalid day of month in start date";$k[$j++]=false;return true}var _b1=$k[--$j];$_.couponstart=$i($G(_b1,0,6));$k[$j++]=_b1;if($_.couponexpire!=-1&&$_.couponexpire<$_.couponstart){$j-=2;$k[$j++]="bwipp.GS1couponExpireDateBeforeStartDate#3646";$k[$j++]="Coupon expires before it starts";$k[$j++]=false;return true}var _b6=$k[--$j];$k[$j++]=$G(_b6,6,_b6.length-6)}}var _b8=$k[$j-1];if(_b8.length>=1){var _b9=$k[$j-1];if($i($G(_b9,0,1))==5){var _bB=$k[--$j];var _bC=$G(_bB,1,_bB.length-1);$k[$j++]=_bC;if(_bC.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortSerialNumberVLI#3658";$k[$j++]="Coupon too short: Missing Serial Number VLI";$k[$j++]=false;return true}var _bD=$k[$j-1];var _bE=$G(_bD,0,1);$k[$j++]=$i(_bE)+6+1;if($i(_bE)+6+1>_bD.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortSerialNumber#3662";$k[$j++]="Coupon too short: Serial Number truncated";$k[$j++]=false;return true}var _bF=$k[--$j];var _bG=$k[--$j];$k[$j++]=$G(_bG,_bF,$f(_bG.length-_bF))}}var _bI=$k[$j-1];if(_bI.length>=1){var _bJ=$k[$j-1];if($i($G(_bJ,0,1))==6){var _bL=$k[--$j];var _bM=$G(_bL,1,_bL.length-1);$k[$j++]=_bM;if(_bM.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortRetailerGCPGLNVLI#3674";$k[$j++]="Coupon too short: Missing Retailer GCP/GLN VLI";$k[$j++]=false;return true}var _bN=$k[$j-1];var _bP=$i($G(_bN,0,1));$k[$j++]=_bP;if(_bP<1||_bP>7){$j-=3;$k[$j++]="bwipp.GS1couponBadRetailerGCPGLNVLI#3677";$k[$j++]="Coupon Retailer GCP/GLN length indicator must be 1-7";$k[$j++]=false;return true}var _bQ=$k[--$j];var _bR=$k[$j-1];$k[$j++]=_bQ+6+1;if(_bQ+6+1>_bR.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortRetailerGCPGLN#3681";$k[$j++]="Coupon too short: Retailer GCP/GLN truncated";$k[$j++]=false;return true}var _bS=$k[--$j];var _bT=$k[--$j];$k[$j++]=$G(_bT,_bS,$f(_bT.length-_bS))}}var _bV=$k[$j-1];if(_bV.length>=1){var _bW=$k[$j-1];if($i($G(_bW,0,1))==9){var _bY=$k[--$j];var _bZ=$G(_bY,1,_bY.length-1);$k[$j++]=_bZ;if(_bZ.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortSaveValueCode#3693";$k[$j++]="Coupon too short: Missing Save Value Code";$k[$j++]=false;return true}var _ba=$k[$j-1];var _bc=$i($G(_ba,0,1));if(_bc>6||(_bc==3||_bc==4)){$j-=2;$k[$j++]="bwipp.GS1couponBadSaveValueCode#3696";$k[$j++]="Coupon Save Value Code must be 0,1,2,5 or 6";$k[$j++]=false;return true}var _bd=$k[--$j];var _be=$G(_bd,1,_bd.length-1);$k[$j++]=_be;if(_be.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortSaveValueAppliesToItem#3702";$k[$j++]="Coupon too short: Missing Save Value Applies to Item";$k[$j++]=false;return true}var _bf=$k[$j-1];if($i($G(_bf,0,1))>2){$j-=2;$k[$j++]="bwipp.GS1couponBadSaveValueAppliesToItem#3705";$k[$j++]="Coupon Save Value Applies to Item must be 0-2";$k[$j++]=false;return true}var _bh=$k[--$j];var _bi=$G(_bh,1,_bh.length-1);$k[$j++]=_bi;if(_bi.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortStoreCouponFlag#3711";$k[$j++]="Coupon too short: Missing Store Coupon Flag";$k[$j++]=false;return true}var _bj=$k[--$j];var _bk=$G(_bj,1,_bj.length-1);$k[$j++]=_bk;if(_bk.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortDontMultiplyFlag#3717";$k[$j++]="Coupon too short: Missing Don't Multiply Flag";$k[$j++]=false;return true}var _bl=$k[$j-1];if($i($G(_bl,0,1))>1){$j-=2;$k[$j++]="bwipp.GS1couponBadDontMultiplyFlag#3720";$k[$j++]="Don't Multiply Flag must be 0 or 1";$k[$j++]=false;return true}var _bn=$k[--$j];$k[$j++]=$G(_bn,1,_bn.length-1)}}var _bp=$k[$j-1];if(_bp.length!=0){$j-=2;$k[$j++]="bwipp.GS1couponUnrecognisedOptionalField#3727";$k[$j++]="Coupon fields must be 1,2,3,4,5,6 or 9, increasing order";$k[$j++]=false;return true}$j--};$_.lintcouponposoffer=function(){var _bq=$k[$j-1];$k[$j++]=true;$F(_bq,function(){var _br=$k[--$j];if(_br<48||_br>57){$k[$j-1]=false;return true}});var _bs=$k[--$j];if(!_bs){$j-=2;$k[$j++]="bwipp.GS1couponNotNumeric#3736";$k[$j++]="Coupon not numeric";$k[$j++]=false;return true}var _bt=$k[$j-1];if(_bt.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortFormatCode#3739";$k[$j++]="Coupon too short: Missing Format Code";$k[$j++]=false;return true}var _bu=$k[$j-1];var _bv=$G(_bu,0,1);if($ne(_bv,"0")&&$ne(_bv,"1")){$j-=2;$k[$j++]="bwipp.GS1couponBadFormatCode#3742";$k[$j++]="Coupon format must be 0 or 1";$k[$j++]=false;return true}var _bw=$k[--$j];var _bx=$G(_bw,1,_bw.length-1);$k[$j++]=_bx;if(_bx.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortFunderVLI#3747";$k[$j++]="Coupon too short: Missing Funder VLI";$k[$j++]=false;return true}var _by=$k[$j-1];var _c0=$i($G(_by,0,1));$k[$j++]=_c0;if(_c0>6){$j-=3;$k[$j++]="bwipp.GS1couponBadFunderVLI#3750";$k[$j++]="Coupon Funder length indicator must be 0-6";$k[$j++]=false;return true}var _c1=$k[--$j];var _c2=$k[$j-1];$k[$j++]=_c1+6+1;if(_c1+6+1>_c2.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortFunder#3754";$k[$j++]="Coupon too short: Truncated Funder ID";$k[$j++]=false;return true}var _c3=$k[--$j];var _c4=$k[--$j];var _c5=$G(_c4,_c3,$f(_c4.length-_c3));$k[$j++]=_c5;if(_c5.length<6){$j-=2;$k[$j++]="bwipp.GS1couponTooShortOfferCode#3759";$k[$j++]="Coupon too short: Truncated Offer Code";$k[$j++]=false;return true}var _c6=$k[--$j];var _c7=$G(_c6,6,_c6.length-6);$k[$j++]=_c7;if(_c7.length<1){$j-=2;$k[$j++]="bwipp.GS1couponTooShortSnVLI#3764";$k[$j++]="Coupon too short: Missing SN VLI";$k[$j++]=false;return true}var _c8=$k[$j-1];var _c9=$G(_c8,0,1);$k[$j++]=$i(_c9)+6+1;if($i(_c9)+6+1>_c8.length){$j-=3;$k[$j++]="bwipp.GS1couponTooShortSn#3769";$k[$j++]="Coupon too short: Truncated SN";$k[$j++]=false;return true}var _cA=$k[--$j];var _cB=$k[--$j];var _cC=$G(_cB,_cA,$f(_cB.length-_cA));$k[$j++]=_cC;if(_cC.length!=0){$j-=2;$k[$j++]="bwipp.GS1couponTooLong#3773";$k[$j++]="Coupon too long";$k[$j++]=false;return true}$j--};$_.lintlatitude=function(){var _cD=$k[$j-1];if(_cD.length!=10){$k[$j-1]="bwipp.GS1badLatitudeLength#3780";$k[$j++]="Invalid length for a latitude";$k[$j++]=false;return true}var _cE=$k[--$j];if($gt(_cE,"1800000000")){$k[$j-1]="bwipp.GS1badLatitude#3783";$k[$j++]="Invalid value for latitude";$k[$j++]=false;return true}};$_.lintlongitude=function(){var _cF=$k[$j-1];if(_cF.length!=10){$k[$j-1]="bwipp.GS1badLongitudeLength#3788";$k[$j++]="Invalid length for a longitude";$k[$j++]=false;return true}var _cG=$k[--$j];if($gt(_cG,"3600000000")){$k[$j-1]="bwipp.GS1badLongitude#3791";$k[$j++]="Invalid value for longitude";$k[$j++]=false;return true}};$_.lintmediatype=function(){var _cI=$k[--$j];if(!$has($_.aidcmediatype,_cI)){$k[$j-1]="bwipp.GS1UnknownMediaType#3795";$k[$j++]="Unknown AIDC media type";$k[$j++]=false;return true}};$_.lintpackagetype=function(){var _cL=$k[--$j];if(!$has($_.packagetype,_cL)){$k[$j-1]="bwipp.GS1unknownPackageType#3799";$k[$j++]="Unknown package type";$k[$j++]=false;return true}};if(!$_.dontlint){$k[$j++]=true;for(var _cQ=0,_cP=$_.vals.length-1;_cQ<=_cP;_cQ+=1){$_.ai=$g($_.ais,_cQ);$_.val=$g($_.vals,_cQ);if($has($_.gs1syntax,$_.ai)){var _cb=$g($g($_.gs1syntax,$_.ai),"parts");$k[$j++]=_cb;$k[$j++]=0;$F(_cb,function(){var _cc=$k[$j-1];if($g(_cc,"opt")){$k[$j-1]=0}else{var _ce=$k[--$j];$k[$j++]=$g(_ce,"min")}var _cg=$k[--$j];var _ch=$k[--$j];$k[$j++]=$f(_ch+_cg)});var _cj=$k[--$j];if(_cj>$_.val.length){$j-=2;$k[$j++]="bwipp.GS1valueTooShort#3813";$k[$j++]="Too short";$k[$j++]=false;break}var _ck=$k[$j-1];var _cp=0;$F(_ck,function(){var _cl=$k[--$j];_cp=$f(_cp+$g(_cl,"max"))});if(_cp<$_.val.length){$j-=2;$k[$j++]="bwipp.GS1valueTooLong#3816";$k[$j++]="Too long";$k[$j++]=false;break}var _cq=$k[--$j];$F(_cq,function(){var _cr=$k[--$j];$_.props=_cr;var _ct=$g($_.props,"max");var _cu=$_.val;var _cv=_cu.length;if(_ct>_cu.length){var _=_cv;_cv=_ct;_ct=_}$_.eval=$G($_.val,0,_ct);var _d0=$_.eval.length;$_.val=$G($_.val,_d0,$_.val.length-_d0);if($_.eval.length==0){if(!$g($_.props,"opt")){$k[$j-1]="bwipp.GS1valueTooShort#3825";$k[$j++]="Too short";$k[$j++]=false;return true}}else{if($_.eval.length<$g($_.props,"min")){$k[$j-1]="bwipp.GS1valueTooShort#3829";$k[$j++]="Too short";$k[$j++]=false;return true}var _d9=new Map([["N","lintnumeric"],["X","lintcset82"],["Y","lintcset39"],["Z","lintcset64"]]);var _dE=$_[$g(_d9,$g($_.props,"cset"))];$k[$j++]=$_.eval;if(_dE()===true){return true}$F($g($_.props,"linters"),function(){var _dI=$k[--$j];var _dJ=$_[_dI];$k[$j++]=$_.eval;if(_dJ()===true){return true}})}});var _dK=$k[$j-1];if(!_dK){break}if($_.val.length!=0){$k[$j-1]="bwipp.GS1valueTooLong#3838";$k[$j++]="Too long";$k[$j++]=false;break}}else{$k[$j-1]="bwipp.GS1unknownAI#3841";$k[$j++]="Unrecognised AI";$k[$j++]=false;break}}var _dM=$k[--$j];if(!_dM){var _dN=$k[--$j];var _dP=$s(_dN.length+$_.ai.length+5);$P(_dP,0,"AI ");$P(_dP,3,$_.ai);$P(_dP,3+$_.ai.length,": ");$P(_dP,5+$_.ai.length,_dN);$k[$j++]=_dP;bwipp_raiseerror()}$_.aiexists=function(){var _dT=$k[--$j];$_.this=_dT;var _dU=$k[--$j];$_.patt=_dU;for(var _dV=0;_dV<1;_dV++){if($has($_.aivals,$_.patt)){$k[$j++]=true;break}if($_.patt.length==4){if($eq($G($_.patt,3,1),"n")){var _de=$eq($G($_.patt,2,1),"n")?2:3;$_.pfxlen=_de;var _df=$_.ais;$k[$j++]=false;for(var _dg=0,_dh=_df.length;_dg<_dh;_dg++){var _di=$g(_df,_dg);$k[$j++]=_di;if($ne(_di,$_.this)&&_di.length==4){var _dl=$k[--$j];if($eq($G(_dl,0,$_.pfxlen),$G($_.patt,0,$_.pfxlen))){$k[$j-1]=true}}else{$j--}}break}$k[$j++]=false;break}$k[$j++]=false;break}};var _dq=$_.vals;$_.aivals=new Map;for(var _dt=0,_ds=$_.vals.length-1;_dt<=_ds;_dt+=1){$_.ai=$g($_.ais,_dt);$_.val=$g($_.vals,_dt);if($has($_.aivals,$_.ai)){if($ne($g($_.aivals,$_.ai),$_.val)){var _e6=$s($_.ai.length+40);$P(_e6,0,"Repeated AIs (");$P(_e6,14,$_.ai);$P(_e6,14+$_.ai.length,") must have the same value");$k[$j++]="bwipp.GS1repeatedDifferingAIs#3891";$k[$j++]=_e6;bwipp_raiseerror()}}else{$p($_.aivals,$_.ai,$_.val)}}for(var _eE=0,_eD=$_.vals.length-1;_eE<=_eD;_eE+=1){$_.ai=$g($_.ais,_eE);var _eJ=$g($_.gs1syntax,$_.ai);var _eK=$has(_eJ,"ex");$k[$j++]=_eJ;$k[$j++]="ex";if(_eK){var _eL=$k[--$j];var _eM=$k[--$j];$F($g(_eM,_eL),function(){var _eO=$k[--$j];$_.patt=_eO;$k[$j++]=$_.patt;$k[$j++]=$_.ai;$_.aiexists();var _eR=$k[--$j];if(_eR){var _eU=$s($_.ai.length+$_.patt.length+36);$P(_eU,0,"AIs (");$P(_eU,5,$_.ai);$P(_eU,5+$_.ai.length,") and (");$P(_eU,12+$_.ai.length,$_.patt);$P(_eU,12+$_.ai.length+$_.patt.length,") are mutually exclusive");$k[$j++]="bwipp.GS1exclusiveAIs#3911";$k[$j++]=_eU;bwipp_raiseerror()}})}else{$j-=2}if($_.lintreqs){var _ee=$g($_.gs1syntax,$_.ai);var _ef=$has(_ee,"req");$k[$j++]=_ee;$k[$j++]="req";if(_ef){var _eg=$k[--$j];var _eh=$k[--$j];$F($g(_eh,_eg),function(){var _ej=$k[--$j];$k[$j++]=false;$F(_ej,function(){var _ek=$k[--$j];$k[$j++]=true;$F(_ek,function(){$k[$j++]=$_.ai;$_.aiexists();var _em=$k[--$j];if(!_em){$k[$j-1]=false}});var _en=$k[--$j];if(_en){$k[$j-1]=true;return true}});var _eo=$k[--$j];if(!_eo){$k[$j++]=Infinity;$F($g($g($_.gs1syntax,$_.ai),"req"),function(){var _et=$k[--$j];$F(_et,function(){var _eu=$k[--$j];$F(_eu,function(){$k[$j++]="+"});$k[$j-1]=" OR "});$k[$j-1]=" WITH "});$j--;var _ev=$a();$k[$j++]=_ev;$k[$j++]=0;for(var _ew=0,_ex=_ev.length;_ew<_ex;_ew++){var _ez=$k[--$j];$k[$j++]=$f(_ez+$g(_ev,_ew).length)}var _f1=$_.ai.length+49;$k[$j++]=_f1;$k[$j++]=_f1;var _f2=$k[$j-3];var _f3=$k[$j-1];var _f4=$s($f(_f3+_f2));var _f5=$k[$j-2];$j-=3;$k[$j++]=_f4;$k[$j++]=_f5;$r(3,-1);var _f6=$k[--$j];$F(_f6,function(){var _f7=$k[--$j];var _f8=$k[--$j];var _f9=$k[$j-1];$P(_f9,_f8,_f7);$k[$j++]=$f(_f7.length+_f8)});$j--;var _fA=$k[--$j];$P(_fA,0,"One of more requisite AIs for AI (");$P(_fA,34,$_.ai);$P(_fA,34+$_.ai.length,") are missing: ");$k[$j++]="bwipp.GS1missingAIs#3938";$k[$j++]=_fA;bwipp_raiseerror()}})}else{$j-=2}}}}$k[$j++]=$_.ais;$k[$j++]=$_.vals;$k[$j++]=$_.fncs;$_=$__}function bwipp_rsecbinary(){if(!bwipp_rsecbinary.globals){var $__=$_;$_=bwipp_rsecbinary.globals={};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_rsecbinary.globals);var _0=$k[--$j];$_.rsprod=_0;var _1=$k[--$j];$_.lfsr=_1;var _2=$k[--$j];$_.nc=_2;var _3=$k[--$j];$_.coeffs=_3;var _4=$k[--$j];$_.nd=_4;var _5=$k[--$j];$_.getdata=_5;for(var _8=0,_7=$_.nc-1;_8<=_7;_8+=1){$p($_.lfsr,_8,0)}for(var _C=0,_B=$_.nd-1;_C<=_B;_C+=1){$k[$j++]=_C;if($_.getdata()===true){break}var _G=$k[--$j];$k[$j++]=$xo(_G,$g($_.lfsr,0));for(var _J=0,_I=$_.nc-2;_J<=_I;_J+=1){var _K=$_.lfsr;var _O=$g($_.coeffs,$f($_.nc-_J)-1);var _P=$k[$j-1];$k[$j++]=_K;$k[$j++]=_J;$k[$j++]=$g(_K,_J+1);$k[$j++]=_O;$k[$j++]=_P;if($_.rsprod()===true){break}var _R=$k[--$j];var _S=$k[--$j];var _T=$k[--$j];var _U=$k[--$j];$p(_U,_T,$xo(_S,_R))}var _Z=$k[$j-1];$k[$j++]=$_.lfsr;$k[$j++]=$_.nc-1;$k[$j++]=$g($_.coeffs,0);$k[$j++]=_Z;if($_.rsprod()===true){break}var _b=$k[--$j];var _c=$k[--$j];var _d=$k[--$j];$p(_d,_c,_b);$j--}$_=$__}function bwipp_rsecprime(){if(!bwipp_rsecprime.globals){var $__=$_;$_=bwipp_rsecprime.globals={};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_rsecprime.globals);var _0=$k[--$j];$_.p=_0;var _1=$k[--$j];$_.lfsr=_1;var _2=$k[--$j];$_.nc=_2;var _3=$k[--$j];$_.coeffs=_3;var _4=$k[--$j];$_.nd=_4;var _5=$k[--$j];$_.getdata=_5;for(var _8=0,_7=$_.nc-1;_8<=_7;_8+=1){$p($_.lfsr,_8,0)}for(var _C=0,_B=$_.nd-1;_C<=_B;_C+=1){$k[$j++]=_C;if($_.getdata()===true){break}var _G=$k[--$j];$k[$j++]=$f(_G-$g($_.lfsr,0)+$_.p)%$_.p;for(var _L=0,_K=$_.nc-2;_L<=_K;_L+=1){var _M=$_.lfsr;var _Q=$g($_.coeffs,$f($_.nc-_L)-1);var _R=$k[$j-1];$p(_M,_L,$f($g(_M,_L+1)+_Q*_R%$_.p)%$_.p)}var _Y=$k[--$j];$p($_.lfsr,$_.nc-1,$g($_.coeffs,0)*_Y%$_.p)}$_=$__}function bwipp_ean5(){if(!bwipp_ean5.globals){var $__=$_;$_=bwipp_ean5.globals={};$_.ean5_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","112","11"]);$_.ean5_barchars="0123456789";$_.ean5_mirrormaps=$a(["11000","10100","10010","10001","01100","00110","00011","01010","01001","00101"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_ean5.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=3.25;$_.text1yoffset=null;$_.height=-1;$_.addondescent=5;$_.width=0;$_.modunit=1;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=5){$k[$j++]="bwipp.ean5badLength#6471";$k[$j++]="EAN-5 add-on must be 5 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _8=$k[--$j];if(_8<48||_8>57){$k[$j++]="bwipp.ean5badCharacter#6475";$k[$j++]="EAN-5 add-on must contain only digits";bwipp_raiseerror()}});if($_.height==-1){$_.height=.7}$_.descent=$_.addondescent*$_.modunit/72;if($_.text1yoffset==null){$_.text1yoffset=$f($_.height*72+4.75)}$_.checksum=0;for(var _E=0;_E<=4;_E+=1){$_.i=_E;$_.barchar=$g($_.barcode,$_.i)-48;if($_.i%2==0){$_.checksum=$f($_.barchar*3+$_.checksum)}else{$_.checksum=$f($_.barchar*9+$_.checksum)}}$_.checksum=$_.checksum%10;$_.mirrormap=$g($_.ean5_mirrormaps,$_.checksum);$_.sbs=$s(31);$_.txt=$a(5);for(var _S=0;_S<=4;_S+=1){$_.i=_S;if($_.i==0){$P($_.sbs,0,$g($_.ean5_encs,10))}else{$P($_.sbs,($_.i-1)*6+7,$g($_.ean5_encs,11))}$X($_.ean5_barchars,$G($_.barcode,$_.i,1));$j--;var _c=$k[--$j];$_.indx=_c.length;$j-=2;$_.enc=$g($_.ean5_encs,$_.indx);if($g($_.mirrormap,$_.i)==49){$_.enclen=$_.enc.length;$_.revenc=$s($_.enclen);for(var _n=0,_m=$_.enclen-1;_n<=_m;_n+=1){$_.j=_n;$_.char=$g($_.enc,$_.j);$p($_.revenc,$_.enclen-$_.j-1,$_.char)}$_.enc=$_.revenc}$P($_.sbs,$_.i*6+3,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f(($_.i-1)*9+10+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$k[$j++]=Infinity;var _1E=[];var _1A=$_.sbs;for(var _1B=0,_1C=_1A.length;_1B<_1C;_1B++){_1E[_1B]=$g(_1A,_1B)-48}_1E=$a(_1E);var _1I=[];for(var _1F=0;_1F<16;_1F++){_1I[_1F]=$f($_.height+$_.descent)}_1I=$a(_1I);var _1L=[];for(var _1J=0;_1J<16;_1J++){_1L[_1J]=-$_.descent}_1L=$a(_1L);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1E;$k[$j++]="bhs";$k[$j++]=_1I;$k[$j++]="bbs";$k[$j++]=_1L;if($_.includetext){$k[$j++]="txt";$k[$j++]=$_.txt}$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardrightpos";$k[$j++]=5;$k[$j++]="guardrightypos";$k[$j++]=$_.text1yoffset;$k[$j++]="borderleft";$k[$j++]=12;$k[$j++]="borderright";$k[$j++]=5;$k[$j++]="bordertop";$k[$j++]=10;var _1Q=$d();$k[$j++]=_1Q;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_ean2(){if(!bwipp_ean2.globals){var $__=$_;$_=bwipp_ean2.globals={};$_.ean2_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","112","11"]);$_.ean2_barchars="0123456789";$_.ean2_mirrormaps=$a(["00","01","10","11"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_ean2.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=3.25;$_.text1yoffset=null;$_.height=-1;$_.addondescent=5;$_.width=0;$_.modunit=1;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=2){$k[$j++]="bwipp.ean2badLength#6649";$k[$j++]="EAN-2 add-on must be 2 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _8=$k[--$j];if(_8<48||_8>57){$k[$j++]="bwipp.ean2badCharacter#6653";$k[$j++]="EAN-2 add-on must contain only digits";bwipp_raiseerror()}});if($_.height==-1){$_.height=.7}$_.descent=$_.addondescent*$_.modunit/72;if($_.text1yoffset==null){$_.text1yoffset=$f($_.height*72+4.75)}$_.mirrormap=$g($_.ean2_mirrormaps,$i($G($_.barcode,0,2))%4);$_.sbs=$s(13);$_.txt=$a(2);for(var _J=0;_J<=1;_J+=1){$_.i=_J;if($_.i==0){$P($_.sbs,0,$g($_.ean2_encs,10))}else{$P($_.sbs,($_.i-1)*6+7,$g($_.ean2_encs,11))}$X($_.ean2_barchars,$G($_.barcode,$_.i,1));$j--;var _T=$k[--$j];$_.indx=_T.length;$j-=2;$_.enc=$g($_.ean2_encs,$_.indx);if($g($_.mirrormap,$_.i)==49){$_.enclen=$_.enc.length;$_.revenc=$s($_.enclen);for(var _e=0,_d=$_.enclen-1;_e<=_d;_e+=1){$_.j=_e;$_.char=$g($_.enc,$_.j);$p($_.revenc,$_.enclen-$_.j-1,$_.char)}$_.enc=$_.revenc}$P($_.sbs,$_.i*6+3,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f(($_.i-1)*9+10+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$k[$j++]=Infinity;var _15=[];var _11=$_.sbs;for(var _12=0,_13=_11.length;_12<_13;_12++){_15[_12]=$g(_11,_12)-48}_15=$a(_15);var _19=[];for(var _16=0;_16<12;_16++){_19[_16]=$f($_.height+$_.descent)}_19=$a(_19);var _1C=[];for(var _1A=0;_1A<12;_1A++){_1C[_1A]=-$_.descent}_1C=$a(_1C);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_15;$k[$j++]="bhs";$k[$j++]=_19;$k[$j++]="bbs";$k[$j++]=_1C;if($_.includetext){$k[$j++]="txt";$k[$j++]=$_.txt}$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardrightpos";$k[$j++]=5;$k[$j++]="guardrightypos";$k[$j++]=$_.text1yoffset;$k[$j++]="borderleft";$k[$j++]=12;$k[$j++]="borderright";$k[$j++]=5;$k[$j++]="bordertop";$k[$j++]=10;var _1H=$d();$k[$j++]=_1H;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_ean13(){if(!bwipp_ean13.globals){var $__=$_;$_=bwipp_ean13.globals={};$_.ean13_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","111","11111","111"]);$_.ean13_barchars="0123456789";$_.ean13_mirrormaps=$a(["000000","001011","001101","001110","010011","011001","011100","010101","010110","011010"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_ean13.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=-7.75;$_.text1yoffset=-5.25;$_.height=-1;$_.guarddescent=5;$_.addondescent=null;$_.width=0;$_.addongap=12;$_.addontextfont="unset";$_.addontextsize=null;$_.addontextxoffset=null;$_.addontextyoffset=null;$_.modunit=1;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _7=$k[--$j];if(_7){var _8=$k[--$j];$_.barcode=_8;$j--;var _9=$k[--$j];$_.addon=_9}else{$j--;$_.addon=""}if($_.barcode.length!=12&&$_.barcode.length!=13){$k[$j++]="bwipp.ean13badLength#6838";$k[$j++]="EAN-13 must be 12 or 13 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _D=$k[--$j];if(_D<48||_D>57){$k[$j++]="bwipp.ean13badCharacter#6842";$k[$j++]="EAN-13 must contain only digits";bwipp_raiseerror()}});if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.ean13badAddOnLength#6846";$k[$j++]="Add-on for EAN-13 must be 2 or 5 digits";bwipp_raiseerror()}$_.descent=$_.guarddescent*$_.modunit/72;if($_.addondescent==null){$_.addondescent=$_.guarddescent}$_.pad=$s(13);$_.checksum=0;for(var _M=0;_M<=11;_M+=1){$_.i=_M;$_.barchar=$g($_.barcode,$_.i)-48;if($_.i%2==0){$_.checksum=$f($_.barchar+$_.checksum)}else{$_.checksum=$f($_.barchar*3+$_.checksum)}}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==13){if($g($_.barcode,12)!=$_.checksum+48){$k[$j++]="bwipp.ean13badCheckDigit#6875";$k[$j++]="Incorrect EAN-13 check digit provided";bwipp_raiseerror()}}$P($_.pad,0,$_.barcode);$p($_.pad,12,$_.checksum+48);$_.barcode=$_.pad;$_.sbs=$s(59);$_.txt=$a(13);$P($_.sbs,0,$g($_.ean13_encs,10));$_.mirrormap=$g($_.ean13_mirrormaps,$g($_.barcode,0)-48);$p($_.txt,0,$a([$G($_.barcode,0,1),$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size]));for(var _u=1;_u<=6;_u+=1){$_.i=_u;$X($_.ean13_barchars,$G($_.barcode,$_.i,1));$j--;var _y=$k[--$j];$_.indx=_y.length;$j-=2;$_.enc=$g($_.ean13_encs,$_.indx);if($g($_.mirrormap,$_.i-1)==49){$_.enclen=$_.enc.length;$_.revenc=$s($_.enclen);for(var _19=0,_18=$_.enclen-1;_19<=_18;_19+=1){$_.j=_19;$_.char=$g($_.enc,$_.j);$p($_.revenc,$_.enclen-$_.j-1,$_.char)}$_.enc=$_.revenc}$P($_.sbs,($_.i-1)*4+3,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f(($_.i-1)*7+15+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$P($_.sbs,(7-1)*4+3,$g($_.ean13_encs,11));for(var _1Y=7;_1Y<=12;_1Y+=1){$_.i=_1Y;$X($_.ean13_barchars,$G($_.barcode,$_.i,1));$j--;var _1c=$k[--$j];$_.indx=_1c.length;$j-=2;$_.enc=$g($_.ean13_encs,$_.indx);$P($_.sbs,($_.i-1)*4+8,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f(($_.i-1)*7+19+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$P($_.sbs,56,$g($_.ean13_encs,12));$k[$j++]=Infinity;var _1v=$_.sbs;for(var _1w=0,_1x=_1v.length;_1w<_1x;_1w++){$k[$j++]=$g(_1v,_1w)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}if($_.includetext){$_.guardheight=$f($_.height+$_.descent);$k[$j++]=Infinity;$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _25=0;_25<12;_25++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _29=0;_29<12;_29++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;$_.bhs=$a();$k[$j++]=Infinity;$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _2F=0;_2F<12;_2F++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _2I=0;_2I<12;_2I++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;$_.bbs=$a()}else{$k[$j++]=Infinity;for(var _2L=0;_2L<30;_2L++){$k[$j++]=$_.height}$_.bhs=$a();$k[$j++]=Infinity;for(var _2N=0;_2N<30;_2N++){$k[$j++]=0}$_.bbs=$a();$_.txt=$a([])}$_.guardrightypos=-5;if($_.addon.length!=0){$k[$j++]=Infinity;$k[$j++]="includetext";$k[$j++]=true;$k[$j++]="height";$k[$j++]=$_.height;if($_.includetext){var _2R=$k[--$j];$k[$j++]=$f(_2R-.15)}$k[$j++]="addondescent";$k[$j++]=$_.addondescent;$k[$j++]="text1xoffset";if($_.addontextxoffset!=null){$k[$j++]=$_.addontextxoffset}else{$k[$j++]=$f(98.25+$_.addongap)}if($_.addontextyoffset!=null){$k[$j++]="text1yoffset";$k[$j++]=$_.addontextyoffset}$k[$j++]="text1size";if($_.addontextsize!=null){$k[$j++]=$_.addontextsize}else{$k[$j++]=$_.text1size}var _2c=$ne($_.addontextfont,"unset")?$_.addontextfont:$_.text1font;$k[$j++]="text1font";$k[$j++]=_2c;$_.addopts=$d();$_.bwipp_dontdraw=true;if($_.addon.length==2){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean2()}if($_.addon.length==5){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean5()}var _2j=$k[--$j];$_.addcode=_2j;$k[$j++]=Infinity;$q($_.sbs);$k[$j++]=$_.addongap;$q($g($_.addcode,"sbs"));$_.sbs=$a();$k[$j++]=Infinity;$q($_.bhs);$q($g($_.addcode,"bhs"));$_.bhs=$a();$k[$j++]=Infinity;$q($_.bbs);$q($g($_.addcode,"bbs"));$_.bbs=$a();$k[$j++]=Infinity;$q($_.txt);$q($g($_.addcode,"txt"));$_.txt=$a();$_.guardrightypos=$g($_.addcode,"guardrightypos")}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=$_.sbs;$k[$j++]="bhs";$k[$j++]=$_.bhs;$k[$j++]="bbs";$k[$j++]=$_.bbs;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardrightpos";if($_.addon.length==0){$k[$j++]=7}else{$k[$j++]=5}$k[$j++]="guardrightypos";$k[$j++]=$_.guardrightypos;$k[$j++]="borderleft";$k[$j++]=11;$k[$j++]="borderright";if($_.addon.length==0){$k[$j++]=7}else{$k[$j++]=5}$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=5+$_.guarddescent*$_.modunit;var _39=$d();$k[$j++]=_39;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_ean8(){if(!bwipp_ean8.globals){var $__=$_;$_=bwipp_ean8.globals={};$_.ean8_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","111","11111","111"]);$_.ean8_barchars="0123456789";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_ean8.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=7.25;$_.text1yoffset=-5.25;$_.height=-1;$_.guarddescent=5;$_.addondescent=null;$_.width=0;$_.modunit=1;$_.permitaddon=false;$_.addongap=12;$_.addontextfont="unset";$_.addontextsize=null;$_.addontextxoffset=null;$_.addontextyoffset=null;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _6=$k[--$j];if(_6){var _7=$k[--$j];$_.barcode=_7;$j--;var _8=$k[--$j];$_.addon=_8}else{$j--;$_.addon=""}if($_.barcode.length!=7&&$_.barcode.length!=8){$k[$j++]="bwipp.ean8badLength#7112";$k[$j++]="EAN-8 must be 7 or 8 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _C=$k[--$j];if(_C<48||_C>57){$k[$j++]="bwipp.ean8badCharacter#7116";$k[$j++]="EAN-8 must contain only digits";bwipp_raiseerror()}});if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.ean8badAddOnLength#7120";$k[$j++]="Add-on for EAN-8 must be 2 or 5 digits";bwipp_raiseerror()}if(!$_.permitaddon&&$_.addon.length!=0){$k[$j++]="bwipp.ean8badAddOnLength#7123";$k[$j++]='Non-standard use of add-on for EAN-8 requires "permitaddon" option';bwipp_raiseerror()}$_.descent=$_.guarddescent*$_.modunit/72;if($_.addondescent==null){$_.addondescent=$_.guarddescent}$_.pad=$s(8);$_.checksum=0;for(var _N=0;_N<=6;_N+=1){$_.i=_N;$_.barchar=$g($_.barcode,$_.i)-48;if($_.i%2!=0){$_.checksum=$f($_.barchar+$_.checksum)}else{$_.checksum=$f($_.barchar*3+$_.checksum)}}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==8){if($g($_.barcode,7)!=$_.checksum+48){$k[$j++]="bwipp.ean8badCheckDigit#7152";$k[$j++]="Incorrect EAN-8 check digit provided";bwipp_raiseerror()}}$P($_.pad,0,$_.barcode);$p($_.pad,7,$_.checksum+48);$_.barcode=$_.pad;$_.sbs=$s(43);$_.txt=$a(8);$P($_.sbs,0,$g($_.ean8_encs,10));for(var _k=0;_k<=3;_k+=1){$_.i=_k;$X($_.ean8_barchars,$G($_.barcode,$_.i,1));$j--;var _o=$k[--$j];$_.indx=_o.length;$j-=2;$_.enc=$g($_.ean8_encs,$_.indx);$P($_.sbs,$_.i*4+3,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f($_.i*7+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$P($_.sbs,4*4+3,$g($_.ean8_encs,11));for(var _17=4;_17<=7;_17+=1){$_.i=_17;$X($_.ean8_barchars,$G($_.barcode,$_.i,1));$j--;var _1B=$k[--$j];$_.indx=_1B.length;$j-=2;$_.enc=$g($_.ean8_encs,$_.indx);$P($_.sbs,$_.i*4+8,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f($_.i*7+$_.text1xoffset)+4,$_.text1yoffset,$_.text1font,$_.text1size]))}$P($_.sbs,40,$g($_.ean8_encs,12));$k[$j++]=Infinity;var _1U=$_.sbs;for(var _1V=0,_1W=_1U.length;_1V<_1W;_1V++){$k[$j++]=$g(_1U,_1V)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}if($_.includetext){$_.guardheight=$f($_.height+$_.descent);$k[$j++]=Infinity;$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _1e=0;_1e<8;_1e++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _1i=0;_1i<8;_1i++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;$_.bhs=$a();$k[$j++]=Infinity;$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _1o=0;_1o<8;_1o++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _1r=0;_1r<8;_1r++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;$_.bbs=$a()}else{$k[$j++]=Infinity;for(var _1u=0;_1u<22;_1u++){$k[$j++]=$_.height}$_.bhs=$a();$k[$j++]=Infinity;for(var _1w=0;_1w<22;_1w++){$k[$j++]=0}$_.bbs=$a();$_.txt=$a([])}$_.guardrightypos=-5;if($_.addon.length!=0){$k[$j++]=Infinity;$k[$j++]="includetext";$k[$j++]=true;$k[$j++]="height";$k[$j++]=$_.height;if($_.includetext){var _20=$k[--$j];$k[$j++]=$f(_20-.15)}$k[$j++]="addondescent";$k[$j++]=$_.addondescent;$k[$j++]="text1xoffset";if($_.addontextxoffset!=null){$k[$j++]=$_.addontextxoffset}else{$k[$j++]=$f(70.25+$_.addongap)}if($_.addontextyoffset!=null){$k[$j++]="text1yoffset";$k[$j++]=$_.addontextyoffset}$k[$j++]="text1size";if($_.addontextsize!=null){$k[$j++]=$_.addontextsize}else{$k[$j++]=$_.text1size}var _2B=$ne($_.addontextfont,"unset")?$_.addontextfont:$_.text1font;$k[$j++]="text1font";$k[$j++]=_2B;$_.addopts=$d();$_.bwipp_dontdraw=true;if($_.addon.length==2){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean2()}if($_.addon.length==5){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean5()}var _2I=$k[--$j];$_.addcode=_2I;$k[$j++]=Infinity;$q($_.sbs);$k[$j++]=$_.addongap;$q($g($_.addcode,"sbs"));$_.sbs=$a();$k[$j++]=Infinity;$q($_.bhs);$q($g($_.addcode,"bhs"));$_.bhs=$a();$k[$j++]=Infinity;$q($_.bbs);$q($g($_.addcode,"bbs"));$_.bbs=$a();$k[$j++]=Infinity;$q($_.txt);$q($g($_.addcode,"txt"));$_.txt=$a();$_.guardrightypos=$g($_.addcode,"guardrightypos")}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=$_.sbs;$k[$j++]="bhs";$k[$j++]=$_.bhs;$k[$j++]="bbs";$k[$j++]=$_.bbs;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardleftpos";$k[$j++]=7;$k[$j++]="guardleftypos";$k[$j++]=-5;$k[$j++]="guardrightpos";if($_.addon.length==0){$k[$j++]=7}else{$k[$j++]=5}$k[$j++]="guardrightypos";$k[$j++]=$_.guardrightypos;$k[$j++]="borderleft";$k[$j++]=7;$k[$j++]="borderright";if($_.addon.length==0){$k[$j++]=7}else{$k[$j++]=5}$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=5+$_.guarddescent*$_.modunit;var _2i=$d();$k[$j++]=_2i;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_upca(){if(!bwipp_upca.globals){var $__=$_;$_=bwipp_upca.globals={};$_.upca_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","111","11111","111"]);$_.upca_barchars="0123456789";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_upca.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=-5.75;$_.text1yoffset=-5.25;$_.height=-1;$_.guarddescent=5;$_.addondescent=null;$_.width=0;$_.modunit=1;$_.addongap=12;$_.addontextfont="unset";$_.addontextsize=null;$_.addontextxoffset=null;$_.addontextyoffset=null;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _6=$k[--$j];if(_6){var _7=$k[--$j];$_.barcode=_7;$j--;var _8=$k[--$j];$_.addon=_8}else{$j--;$_.addon=""}if($_.barcode.length==7||$_.barcode.length==8){$F($_.barcode,function(){var _C=$k[--$j];if(_C<48||_C>57){$k[$j++]="bwipp.upcAupcEbadCharacter#7377";$k[$j++]="UPC-E must contain only digits";bwipp_raiseerror()}});var _E=$g($_.barcode,0);if(_E!=48&&_E!=49){$k[$j++]="bwipp.upcAupcEbadNumberSystem#7381";$k[$j++]="UPC-E must have number system 0 or 1";bwipp_raiseerror()}for(var _F=0;_F<1;_F++){var _H=$_.barcode.length==8?12:11;$_.upcacode=$s(_H);if($g($_.barcode,6)-48<=2){$P($_.upcacode,0,$G($_.barcode,0,3));$P($_.upcacode,3,$G($_.barcode,6,1));$P($_.upcacode,4,"0000");$P($_.upcacode,8,$G($_.barcode,3,3));break}if($g($_.barcode,6)-48==3){$P($_.upcacode,0,$G($_.barcode,0,4));$P($_.upcacode,4,"00000");$P($_.upcacode,9,$G($_.barcode,4,2));break}if($g($_.barcode,6)-48==4){$P($_.upcacode,0,$G($_.barcode,0,5));$P($_.upcacode,5,"00000");$P($_.upcacode,10,$G($_.barcode,5,1));break}if($g($_.barcode,6)-48>=5){$P($_.upcacode,0,$G($_.barcode,0,6));$P($_.upcacode,6,"0000");$P($_.upcacode,10,$G($_.barcode,6,1));break}}if($_.barcode.length==8){$P($_.upcacode,11,$G($_.barcode,7,1))}$_.barcode=$_.upcacode}if($_.barcode.length!=11&&$_.barcode.length!=12){$k[$j++]="bwipp.upcAbadLength#7421";$k[$j++]="UPC-A must be 11 or 12 digits";bwipp_raiseerror()}var _13=$_.barcode;for(var _14=0,_15=_13.length;_14<_15;_14++){var _16=$g(_13,_14);if(_16<48||_16>57){$k[$j++]="bwipp.upcAbadCharacter#7425";$k[$j++]="UPC-A must contain only digits";bwipp_raiseerror()}}if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.upcAbadAddOnLength#7429";$k[$j++]="Add-on for UPC-A must be 2 or 5 digits";bwipp_raiseerror()}$_.descent=$_.guarddescent*$_.modunit/72;if($_.addondescent==null){$_.addondescent=$_.guarddescent}$_.pad=$s(12);$_.checksum=0;for(var _1F=0;_1F<=10;_1F+=1){$_.i=_1F;$_.barchar=$g($_.barcode,$_.i)-48;if($_.i%2!=0){$_.checksum=$_.checksum+$_.barchar}else{$_.checksum=$_.checksum+$_.barchar*3}}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==12){if($g($_.barcode,11)!=$_.checksum+48){$k[$j++]="bwipp.upcAbadCheckDigit#7458";$k[$j++]="Incorrect UPC check digit provided";bwipp_raiseerror()}}$P($_.pad,0,$_.barcode);$p($_.pad,11,$_.checksum+48);$_.barcode=$_.pad;$_.sbs=$s(59);$_.txt=$a(12);$P($_.sbs,0,$g($_.upca_encs,10));for(var _1c=0;_1c<=5;_1c+=1){$_.i=_1c;$X($_.upca_barchars,$G($_.barcode,$_.i,1));$j--;var _1g=$k[--$j];$_.indx=_1g.length;$j-=2;$_.enc=$g($_.upca_encs,$_.indx);$P($_.sbs,$_.i*4+3,$_.enc);if($_.i==0){$p($_.txt,0,$a([$G($_.barcode,0,1),$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size-2]))}else{$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f($_.i*7+13+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}}$P($_.sbs,6*4+3,$g($_.upca_encs,11));for(var _28=6;_28<=11;_28+=1){$_.i=_28;$X($_.upca_barchars,$G($_.barcode,$_.i,1));$j--;var _2C=$k[--$j];$_.indx=_2C.length;$j-=2;$_.enc=$g($_.upca_encs,$_.indx);$P($_.sbs,$_.i*4+8,$_.enc);if($_.i==11){$p($_.txt,11,$a([$G($_.barcode,11,1),$_.text1xoffset+107,$_.text1yoffset,$_.text1font,$_.text1size-2]))}else{$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f($_.i*7+$_.text1xoffset)+17,$_.text1yoffset,$_.text1font,$_.text1size]))}}$P($_.sbs,56,$g($_.upca_encs,12));$k[$j++]=Infinity;var _2e=$_.sbs;for(var _2f=0,_2g=_2e.length;_2f<_2g;_2f++){$k[$j++]=$g(_2e,_2f)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}if($_.includetext){$_.guardheight=$f($_.height+$_.descent);$k[$j++]=Infinity;for(var _2m=0;_2m<4;_2m++){$k[$j++]=$_.guardheight}for(var _2o=0;_2o<10;_2o++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _2s=0;_2s<10;_2s++){$k[$j++]=$_.height}for(var _2u=0;_2u<4;_2u++){$k[$j++]=$_.guardheight}$_.bhs=$a();$k[$j++]=Infinity;for(var _2w=0;_2w<4;_2w++){$k[$j++]=-$_.descent}for(var _2y=0;_2y<10;_2y++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _31=0;_31<10;_31++){$k[$j++]=0}for(var _32=0;_32<4;_32++){$k[$j++]=-$_.descent}$_.bbs=$a()}else{$k[$j++]=Infinity;for(var _34=0;_34<30;_34++){$k[$j++]=$_.height}$_.bhs=$a();$k[$j++]=Infinity;for(var _36=0;_36<30;_36++){$k[$j++]=0}$_.bbs=$a();$_.txt=$a([])}$_.guardrightypos=-5;if($_.addon.length!=0){$k[$j++]=Infinity;$k[$j++]="includetext";$k[$j++]=true;$k[$j++]="height";$k[$j++]=$_.height;if($_.includetext){var _3A=$k[--$j];$k[$j++]=$f(_3A-.15)}$k[$j++]="addondescent";$k[$j++]=$_.addondescent;$k[$j++]="text1xoffset";if($_.addontextxoffset!=null){$k[$j++]=$_.addontextxoffset}else{$k[$j++]=$f(98.25+$_.addongap)}if($_.addontextyoffset!=null){$k[$j++]="text1yoffset";$k[$j++]=$_.addontextyoffset}$k[$j++]="text1size";if($_.addontextsize!=null){$k[$j++]=$_.addontextsize}else{$k[$j++]=$_.text1size}var _3L=$ne($_.addontextfont,"unset")?$_.addontextfont:$_.text1font;$k[$j++]="text1font";$k[$j++]=_3L;$_.addopts=$d();$_.bwipp_dontdraw=true;if($_.addon.length==2){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean2()}if($_.addon.length==5){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean5()}var _3S=$k[--$j];$_.addcode=_3S;$k[$j++]=Infinity;$q($_.sbs);$k[$j++]=$_.addongap;$q($g($_.addcode,"sbs"));$_.sbs=$a();$k[$j++]=Infinity;$q($_.bhs);$q($g($_.addcode,"bhs"));$_.bhs=$a();$k[$j++]=Infinity;$q($_.bbs);$q($g($_.addcode,"bbs"));$_.bbs=$a();$k[$j++]=Infinity;$q($_.txt);$q($g($_.addcode,"txt"));$_.txt=$a();$_.guardrightypos=$g($_.addcode,"guardrightypos")}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=$_.sbs;$k[$j++]="bhs";$k[$j++]=$_.bhs;$k[$j++]="bbs";$k[$j++]=$_.bbs;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardrightpos";if($_.addon.length==0){$k[$j++]=9}else{$k[$j++]=5}$k[$j++]="guardrightypos";$k[$j++]=$_.guardrightypos;$k[$j++]="borderleft";$k[$j++]=9;$k[$j++]="borderright";if($_.addon.length==0){$k[$j++]=9}else{$k[$j++]=5}$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=5+$_.guarddescent*$_.modunit;var _3s=$d();$k[$j++]=_3s;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_upce(){if(!bwipp_upce.globals){var $__=$_;$_=bwipp_upce.globals={};$_.upce_encs=$a(["3211","2221","2122","1411","1132","1231","1114","1312","1213","3112","111","111111"]);$_.upce_barchars="0123456789";$_.upce_mirrormaps=$a(["000111","001011","001101","001110","010011","011001","011100","010101","010110","011010"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_upce.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=12;$_.text1xoffset=-5.75;$_.text1yoffset=-5.25;$_.height=-1;$_.guarddescent=5;$_.addondescent=null;$_.width=0;$_.modunit=1;$_.addongap=12;$_.addontextfont="unset";$_.addontextsize=null;$_.addontextxoffset=null;$_.addontextyoffset=null;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _7=$k[--$j];if(_7){var _8=$k[--$j];$_.barcode=_8;$j--;var _9=$k[--$j];$_.addon=_9}else{$j--;$_.addon=""}if($_.barcode.length==11||$_.barcode.length==12){$F($_.barcode,function(){var _D=$k[--$j];if(_D<48||_D>57){$k[$j++]="bwipp.upcEupcAbadCharacter#7694";$k[$j++]="UPC-A must contain only digits";bwipp_raiseerror()}});for(var _E=0;_E<1;_E++){var _G=$_.barcode.length==12?8:7;$_.upcecode=$s(_G);if($g($_.barcode,3)-48<=2&&$eq($G($_.barcode,4,4),"0000")){$P($_.upcecode,0,$G($_.barcode,0,3));$P($_.upcecode,3,$G($_.barcode,8,3));$P($_.upcecode,6,$G($_.barcode,3,1));break}if($eq($G($_.barcode,4,5),"00000")){$P($_.upcecode,0,$G($_.barcode,0,4));$P($_.upcecode,4,$G($_.barcode,9,2));$P($_.upcecode,6,"3");break}if($eq($G($_.barcode,5,5),"00000")){$P($_.upcecode,0,$G($_.barcode,0,5));$P($_.upcecode,5,$G($_.barcode,10,1));$P($_.upcecode,6,"4");break}if($g($_.barcode,10)-48>=5&&$eq($G($_.barcode,6,4),"0000")){$P($_.upcecode,0,$G($_.barcode,0,6));$P($_.upcecode,6,$G($_.barcode,10,1));break}$k[$j++]="bwipp.upcEupcAnotCompressible#7722";$k[$j++]="UPC-A cannot be converted to a UPC-E";bwipp_raiseerror()}if($_.barcode.length==12){$P($_.upcecode,7,$G($_.barcode,11,1))}$_.barcode=$_.upcecode}if($_.barcode.length!=7&&$_.barcode.length!=8){$k[$j++]="bwipp.upcEbadLength#7734";$k[$j++]="UPC-E must be 7 or 8 digits";bwipp_raiseerror()}var _14=$_.barcode;for(var _15=0,_16=_14.length;_15<_16;_15++){var _17=$g(_14,_15);if(_17<48||_17>57){$k[$j++]="bwipp.upcEbadCharacter#7738";$k[$j++]="UPC-E must contain only digits";bwipp_raiseerror()}}if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.upcEbadAddOnLength#7742";$k[$j++]="Add-on for UPC-E must be 2 or 5 digits";bwipp_raiseerror()}var _1C=$g($_.barcode,0);if(_1C!=48&&_1C!=49){$k[$j++]="bwipp.upcEbadNumberSystem#7749";$k[$j++]="UPC-E must have number system 0 or 1";bwipp_raiseerror()}$_.descent=$_.guarddescent*$_.modunit/72;if($_.addondescent==null){$_.addondescent=$_.guarddescent}for(var _1H=0;_1H<1;_1H++){$_.upcacode=$s(11);if($g($_.barcode,6)-48<=2){$P($_.upcacode,0,$G($_.barcode,0,3));$P($_.upcacode,3,$G($_.barcode,6,1));$P($_.upcacode,4,"0000");$P($_.upcacode,8,$G($_.barcode,3,3));break}if($g($_.barcode,6)-48==3){$P($_.upcacode,0,$G($_.barcode,0,4));$P($_.upcacode,4,"00000");$P($_.upcacode,9,$G($_.barcode,4,2));break}if($g($_.barcode,6)-48==4){$P($_.upcacode,0,$G($_.barcode,0,5));$P($_.upcacode,5,"00000");$P($_.upcacode,10,$G($_.barcode,5,1));break}if($g($_.barcode,6)-48>=5){$P($_.upcacode,0,$G($_.barcode,0,6));$P($_.upcacode,6,"0000");$P($_.upcacode,10,$G($_.barcode,6,1));break}}$_.checksum=0;for(var _1w=0;_1w<=10;_1w+=1){$_.i=_1w;$_.barchar=$g($_.upcacode,$_.i)-48;if($_.i%2!=0){$_.checksum=$_.checksum+$_.barchar}else{$_.checksum=$_.checksum+$_.barchar*3}}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==8){if($g($_.barcode,7)!=$_.checksum+48){$k[$j++]="bwipp.upcEbadCheckDigit#7805";$k[$j++]="Incorrect UPC check digit provided";bwipp_raiseerror()}}$_.pad=$s(8);$P($_.pad,0,$_.barcode);$p($_.pad,7,$_.checksum+48);$_.barcode=$_.pad;$_.txt=$a(8);$p($_.txt,0,$a([$G($_.barcode,0,1),$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size-2]));$_.mirrormap=$g($_.upce_mirrormaps,$_.checksum);if($g($_.barcode,0)==48){$_.invt=$s($_.mirrormap.length);for(var _2X=0,_2W=$_.mirrormap.length-1;_2X<=_2W;_2X+=1){$_.i=_2X;if($g($_.mirrormap,$_.i)==48){$p($_.invt,$_.i,49)}else{$p($_.invt,$_.i,48)}}$_.mirrormap=$_.invt}$_.sbs=$s(33);$P($_.sbs,0,$g($_.upce_encs,10));for(var _2j=1;_2j<=6;_2j+=1){$_.i=_2j;$X($_.upce_barchars,$G($_.barcode,$_.i,1));$j--;var _2n=$k[--$j];$_.indx=_2n.length;$j-=2;$_.enc=$g($_.upce_encs,$_.indx);if($g($_.mirrormap,$_.i-1)==49){$_.enclen=$_.enc.length;$_.revenc=$s($_.enclen);for(var _2y=0,_2x=$_.enclen-1;_2y<=_2x;_2y+=1){$_.j=_2y;$_.char=$g($_.enc,$_.j);$p($_.revenc,$_.enclen-$_.j-1,$_.char)}$_.enc=$_.revenc}$P($_.sbs,($_.i-1)*4+3,$_.enc);$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),$f(($_.i-1)*7+13+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size]))}$p($_.txt,7,$a([$G($_.barcode,7,1),$f(6*7+21+$_.text1xoffset),$_.text1yoffset,$_.text1font,$_.text1size-2]));$P($_.sbs,27,$g($_.upce_encs,11));$k[$j++]=Infinity;var _3V=$_.sbs;for(var _3W=0,_3X=_3V.length;_3W<_3X;_3W++){$k[$j++]=$g(_3V,_3W)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}if($_.includetext){$_.guardheight=$f($_.height+$_.descent);$k[$j++]=Infinity;$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;for(var _3f=0;_3f<12;_3f++){$k[$j++]=$_.height}$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;$k[$j++]=$_.guardheight;$_.bhs=$a();$k[$j++]=Infinity;$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;for(var _3m=0;_3m<12;_3m++){$k[$j++]=0}$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;$k[$j++]=-$_.descent;$_.bbs=$a()}else{$k[$j++]=Infinity;for(var _3q=0;_3q<17;_3q++){$k[$j++]=$_.height}$_.bhs=$a();$k[$j++]=Infinity;for(var _3s=0;_3s<17;_3s++){$k[$j++]=0}$_.bbs=$a();$_.txt=$a([])}$_.guardrightypos=-5;if($_.addon.length!=0){$k[$j++]=Infinity;$k[$j++]="includetext";$k[$j++]=true;$k[$j++]="height";$k[$j++]=$_.height;if($_.includetext){var _3w=$k[--$j];$k[$j++]=$f(_3w-.15)}$k[$j++]="addondescent";$k[$j++]=$_.addondescent;$k[$j++]="text1xoffset";if($_.addontextxoffset!=null){$k[$j++]=$_.addontextxoffset}else{$k[$j++]=$f(54.25+$_.addongap)}if($_.addontextyoffset!=null){$k[$j++]="text1yoffset";$k[$j++]=$_.addontextyoffset}$k[$j++]="text1size";if($_.addontextsize!=null){$k[$j++]=$_.addontextsize}else{$k[$j++]=$_.text1size}var _47=$ne($_.addontextfont,"unset")?$_.addontextfont:$_.text1font;$k[$j++]="text1font";$k[$j++]=_47;$_.addopts=$d();$_.bwipp_dontdraw=true;if($_.addon.length==2){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean2()}if($_.addon.length==5){$k[$j++]=$_.addon;$k[$j++]=$_.addopts;bwipp_ean5()}var _4E=$k[--$j];$_.addcode=_4E;$k[$j++]=Infinity;$q($_.sbs);$k[$j++]=$_.addongap;$q($g($_.addcode,"sbs"));$_.sbs=$a();$k[$j++]=Infinity;$q($_.bhs);$q($g($_.addcode,"bhs"));$_.bhs=$a();$k[$j++]=Infinity;$q($_.bbs);$q($g($_.addcode,"bbs"));$_.bbs=$a();$k[$j++]=Infinity;$q($_.txt);$q($g($_.addcode,"txt"));$_.txt=$a();$_.guardrightypos=$g($_.addcode,"guardrightypos")}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=$_.sbs;$k[$j++]="bhs";$k[$j++]=$_.bhs;$k[$j++]="bbs";$k[$j++]=$_.bbs;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="opt";$k[$j++]=$_.options;$k[$j++]="guardrightpos";if($_.addon.length==0){$k[$j++]=9}else{$k[$j++]=5}$k[$j++]="guardrightypos";$k[$j++]=$_.guardrightypos;$k[$j++]="borderleft";$k[$j++]=9;$k[$j++]="borderright";if($_.addon.length==0){$k[$j++]=9}else{$k[$j++]=5}$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=5+$_.guarddescent*$_.modunit;var _4e=$d();$k[$j++]=_4e;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_isbn(){var $__=$_;$_=Object.assign({},$_,bwipp_isbn.globals);$_.dontdraw=false;$_.includetext=false;$_.isbntextfont="OCR-A";$_.isbntextsize=8;$_.isbntextxoffset=null;$_.isbntextyoffset=null;$_.height=-1;$_.width=0;$_.addongap=12;$_.legacy=false;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.barcode=_6;$j--;var _7=$k[--$j];$_.addon=_7}else{$j--;$_.addon=""}if($_.barcode.length!=15&&$_.barcode.length!=17&&($_.barcode.length!=11&&$_.barcode.length!=13)){$k[$j++]="bwipp.isbnBadLength#8021";$k[$j++]="ISBN-13 must be 15 or 17 characters including dashes. ISBN-10 must be 11 or 13 characters including dashes";bwipp_raiseerror()}if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.isbnBadAddOnLength#8024";$k[$j++]="Add-on for ISBN must be 2 or 5 digits";bwipp_raiseerror()}if($_.barcode.length>=15){var _H=$G($_.barcode,0,4);if($ne(_H,"978-")&&$ne(_H,"979-")){$k[$j++]="bwipp.isbn13badPrefix#8028";$k[$j++]="ISBN-13 prefix must be 978- or 979-";bwipp_raiseerror()}$_.wasdash=false;$_.numdash=0;$_.numdigit=0;var _J=$G($_.barcode,5,9);for(var _K=0,_L=_J.length;_K<_L;_K++){var _M=$g(_J,_K);$k[$j++]=_M;if(_M==45){if($_.wasdash){$k[$j-1]="bwipp.isbn13adjacentDashes#8035";$k[$j++]="ISBN-13 does not permit adjacent dashes";bwipp_raiseerror()}$_.wasdash=true;$_.numdash=$_.numdash+1}var _P=$k[--$j];if(_P>=48&&_P<=57){$_.wasdash=false;$_.numdigit=$_.numdigit+1}}if($_.numdash!=2||$_.numdigit!=7){$k[$j++]="bwipp.isbn13numDashesDigits#8046";$k[$j++]="Incorrect number of dashes and digits for ISBN-13";bwipp_raiseerror()}var _U=$g($_.barcode,14);if(_U<48||_U>57){$k[$j++]="bwipp.isbn13character15#8049";$k[$j++]="ISBN-13 character 15 must be a digit";bwipp_raiseerror()}if($_.barcode.length==17){if($ne($G($_.barcode,15,1),"-")){$k[$j++]="bwipp.isbn13character16#8053";$k[$j++]="ISBN-13 penultimate character must be a dash";bwipp_raiseerror()}var _Z=$g($_.barcode,16);if(_Z<48||_Z>57){$k[$j++]="bwipp.isbn13character17#8056";$k[$j++]="ISBN-13 final character must be a digit";bwipp_raiseerror()}}}else{var _b=$g($_.barcode,0);if(_b<48||_b>57){$k[$j++]="bwipp.isbn10FirstDigit#8061";$k[$j++]="ISBN-10 first character must be a digit";bwipp_raiseerror()}$_.wasdash=false;$_.numdash=0;$_.numdigit=0;var _d=$G($_.barcode,1,9);for(var _e=0,_f=_d.length;_e<_f;_e++){var _g=$g(_d,_e);$k[$j++]=_g;if(_g==45){if($_.wasdash){$k[$j-1]="bwipp.isbn10adjacentDashes#8068";$k[$j++]="ISBN-10 does not permit adjacent dashes";bwipp_raiseerror()}$_.wasdash=true;$_.numdash=$_.numdash+1}var _j=$k[--$j];if(_j>=48&&_j<=57){$_.wasdash=false;$_.numdigit=$_.numdigit+1}}if($_.numdash!=2||$_.numdigit!=7){$k[$j++]="bwipp.isbn10numDashesDigits#8079";$k[$j++]="Incorrect number of dashes and digits for ISBN-10";bwipp_raiseerror()}var _o=$g($_.barcode,10);if(_o<48||_o>57){$k[$j++]="bwipp.isbn10character11#8082";$k[$j++]="ISBN-10 character 11 must be a digit";bwipp_raiseerror()}if($_.barcode.length==13){if($ne($G($_.barcode,11,1),"-")){$k[$j++]="bwipp.isbn10character12#8086";$k[$j++]="ISBN-10 penultimate character must be a dash";bwipp_raiseerror()}var _t=$g($_.barcode,12);if((_t<48||_t>57)&&_t!=88){$k[$j++]="bwipp.isbn10character13#8089";$k[$j++]="ISBN-10 final character must be a digit or X";bwipp_raiseerror()}}}$_.isbntxt=$_.barcode;if($_.isbntxt.length<=13){$_.isbn=$s(10);$_.checksum=0;$_.i=0;$_.n=0;for(;;){$_.isbnchar=$g($_.isbntxt,$_.i)-48;if($_.isbnchar!=-3){$p($_.isbn,$_.n,$_.isbnchar+48);if($_.n<9){$_.checksum=$f($_.checksum+(10-$_.n)*$_.isbnchar)}$_.n=$_.n+1}$_.i=$_.i+1;if($_.i==$_.isbntxt.length){break}}var _1D=(11-$_.checksum%11)%11;if(_1D==10){_1D=40}$_.checksum=_1D+48;$_.isbn=$G($_.isbn,0,9);if($_.isbntxt.length==13){if($g($_.isbntxt,12)!=$_.checksum){$k[$j++]="bwipp.isbn10badCheckDigit#8119";$k[$j++]="Incorrect ISBN-10 check digit provided";bwipp_raiseerror()}}}if(!$_.legacy||$_.isbntxt.length>=15){if($_.isbntxt.length<=13){$_.pad=$s(15);$P($_.pad,0,"978-");$P($_.pad,4,$G($_.isbntxt,0,11));$_.isbntxt=$_.pad}$_.isbn=$s(13);$_.checksum=0;$_.i=0;$_.n=0;for(;;){$_.isbnchar=$g($_.isbntxt,$_.i)-48;if($_.isbnchar!=-3){$p($_.isbn,$_.n,$_.isbnchar+48);if($_.n<12){var _1i=$_.isbnchar;if($_.n%2!=0){_1i=_1i*3}$_.checksum=$f(_1i+$_.checksum)}$_.n=$_.n+1}$_.i=$_.i+1;if($_.i==$_.isbntxt.length){break}}$_.checksum=(10-$_.checksum%10)%10+48;$_.isbn=$G($_.isbn,0,12);if($_.isbntxt.length==17){if($g($_.isbntxt,16)!=$_.checksum){$k[$j++]="bwipp.isbn13badCheckDigit#8158";$k[$j++]="Incorrect ISBN-13 check digit provided";bwipp_raiseerror()}}}var _1w=$_.isbn.length==12?22:18;$_.pad=$s(_1w);$P($_.pad,0,"ISBN ");$P($_.pad,5,$_.isbntxt);$p($_.pad,$_.pad.length-2,45);$p($_.pad,$_.pad.length-1,$_.checksum);$_.isbntxt=$_.pad;$_.barcode=$s(12);if($_.isbn.length==9){$P($_.barcode,0,"978");$P($_.barcode,3,$_.isbn)}else{$P($_.barcode,0,$_.isbn)}if($_.addon.length!=0){var _2G=$s(12+$_.addon.length+1);$P(_2G,0,$_.barcode);$P(_2G,12," ");$P(_2G,13,$_.addon);$_.barcode=_2G}$p($_.options,"addongap",$_.addongap);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_ean13();var _2N=$k[--$j];$_.args=_2N;if($_.includetext){if($_.isbntextxoffset==null){var _2R=$_.isbn.length==9?-1:-12;$_.isbntextxoffset=_2R}if($_.isbntextyoffset==null){$_.isbntextyoffset=$g($g($_.args,"bhs"),2)*72+3}if($has($_.args,"txt")){$_.txt=$g($_.args,"txt");$_.newtxt=$a($_.txt.length+1);$P($_.newtxt,0,$_.txt);$p($_.newtxt,$_.newtxt.length-1,$a([$_.isbntxt,$_.isbntextxoffset,$_.isbntextyoffset,$_.isbntextfont,$_.isbntextsize]));$p($_.args,"txt",$_.newtxt)}else{$p($_.args,"txt",$a([$a([$_.isbntxt,$_.isbntextxoffset,$_.isbntextyoffset,$_.isbntextfont,$_.isbntextsize])]))}}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_ismn(){var $__=$_;$_=Object.assign({},$_,bwipp_ismn.globals);$_.dontdraw=false;$_.includetext=false;$_.ismntextfont="OCR-A";$_.ismntextsize=8;$_.ismntextxoffset=null;$_.ismntextyoffset=null;$_.height=-1;$_.width=0;$_.addongap=12;$_.legacy=false;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.barcode=_6;$j--;var _7=$k[--$j];$_.addon=_7}else{$j--;$_.addon=""}if($_.barcode.length!=15&&$_.barcode.length!=17&&($_.barcode.length!=11&&$_.barcode.length!=13)){$k[$j++]="bwipp.ismnBadLength#8334";$k[$j++]="ISMN-13 must be 15 or 17 characters including dashes. ISMN-10 must be 11 or 13 characters including dashes";bwipp_raiseerror()}if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.ismnBadAddOnLength#8337";$k[$j++]="Add-on for ISMN must be 2 or 5 digits";bwipp_raiseerror()}if($_.barcode.length>=15){if($ne($G($_.barcode,0,4),"979-")){$k[$j++]="bwipp.ismn13badPrefix#8341";$k[$j++]="ISMN-13 prefix must be 979-";bwipp_raiseerror()}$_.wasdash=false;$_.numdash=0;$_.numdigit=0;var _J=$G($_.barcode,5,9);for(var _K=0,_L=_J.length;_K<_L;_K++){var _M=$g(_J,_K);$k[$j++]=_M;if(_M==45){if($_.wasdash){$k[$j-1]="bwipp.ismn13adjacentDashes#8348";$k[$j++]="ISMN-13 does not permit adjacent dashes";bwipp_raiseerror()}$_.wasdash=true;$_.numdash=$_.numdash+1}var _P=$k[--$j];if(_P>=48&&_P<=57){$_.wasdash=false;$_.numdigit=$_.numdigit+1}}if($_.numdash!=2||$_.numdigit!=7){$k[$j++]="bwipp.ismn13numDashesDigits#8359";$k[$j++]="Incorrect number of dashes and digits for ISMN-13";bwipp_raiseerror()}var _U=$g($_.barcode,14);if(_U<48||_U>57){$k[$j++]="bwipp.ismn13character15#8362";$k[$j++]="ISMN-13 character 15 must be a digit";bwipp_raiseerror()}if($_.barcode.length==17){if($ne($G($_.barcode,15,1),"-")){$k[$j++]="bwipp.ismn13character16#8366";$k[$j++]="ISMN-13 penultimate character must be a dash";bwipp_raiseerror()}var _Z=$g($_.barcode,16);if(_Z<48||_Z>57){$k[$j++]="bwipp.ismn13character17#8369";$k[$j++]="ISMN-13 final character must be a digit";bwipp_raiseerror()}}}else{if($ne($G($_.barcode,0,2),"M-")){$k[$j++]="bwipp.ismn10badPrefix#8374";$k[$j++]="ISMN-10 prefix must be M-";bwipp_raiseerror()}var _d=$g($_.barcode,2);if(_d<48||_d>57){$k[$j++]="bwipp.ismn10character3#8377";$k[$j++]="ISMN-10 character 3 must be a digit";bwipp_raiseerror()}$_.wasdash=false;$_.numdash=0;$_.numdigit=0;var _f=$G($_.barcode,3,7);for(var _g=0,_h=_f.length;_g<_h;_g++){var _i=$g(_f,_g);$k[$j++]=_i;if(_i==45){if($_.wasdash){$k[$j-1]="bwipp.ismn10adjacentDashes#8384";$k[$j++]="ISMN-10 does not permit adjacent dashes";bwipp_raiseerror()}$_.wasdash=true;$_.numdash=$_.numdash+1}var _l=$k[--$j];if(_l>=48&&_l<=57){$_.wasdash=false;$_.numdigit=$_.numdigit+1}}if($_.numdash!=1||$_.numdigit!=6){$k[$j++]="bwipp.ismn10numDashesDigits#8395";$k[$j++]="Incorrect number of dashes and digits for ISMN-10";bwipp_raiseerror()}var _q=$g($_.barcode,10);if(_q<48||_q>57){$k[$j++]="bwipp.ismn10character11#8398";$k[$j++]="ISMN-10 character 11 must be a digit";bwipp_raiseerror()}if($_.barcode.length==13){if($ne($G($_.barcode,11,1),"-")){$k[$j++]="bwipp.ismn10character12#8402";$k[$j++]="ISMN-10 penultimate character must be a dash";bwipp_raiseerror()}var _v=$g($_.barcode,12);if((_v<48||_v>57)&&_v!=88){$k[$j++]="bwipp.ismn10character13#8405";$k[$j++]="ISMN-10 final character must be a digit or X";bwipp_raiseerror()}}}$_.ismntxt=$_.barcode;$_.legacytxt="";if($_.ismntxt.length<=13){$_.legacytxt=$_.ismntxt;$_.pad=$s($_.ismntxt.length+4);$P($_.pad,0,"979-0-");$P($_.pad,6,$G($_.ismntxt,2,$_.ismntxt.length-2));$_.ismntxt=$_.pad}$_.ismn=$s(13);$_.checksum=0;$_.i=0;$_.n=0;for(;;){$_.ismnchar=$g($_.ismntxt,$_.i)-48;if($_.ismnchar!=-3){$p($_.ismn,$_.n,$_.ismnchar+48);if($_.n<12){if($_.n%2==0){$_.checksum=$_.ismnchar+$_.checksum}else{$_.checksum=$_.ismnchar*3+$_.checksum}}$_.n=$_.n+1}$_.i=$_.i+1;if($_.i==$_.ismntxt.length){break}}$_.checksum=(10-$_.checksum%10)%10+48;if($_.barcode.length==13||$_.barcode.length==17){var _1S=$_.barcode;if($g(_1S,_1S.length-1)!=$_.checksum){$k[$j++]="bwipp.ismnBadCheckDigit#8453";$k[$j++]="Incorrect ISMN check digit provided";bwipp_raiseerror()}}if($_.legacy&&$_.legacytxt.length!=0){$_.ismntxt=$_.legacytxt;$_.pad=$s(18)}else{$_.pad=$s(22)}$P($_.pad,0,"ISMN ");$P($_.pad,5,$_.ismntxt);$p($_.pad,$_.pad.length-2,45);$p($_.pad,$_.pad.length-1,$_.checksum);$_.ismntxt=$_.pad;$_.barcode=$G($_.ismn,0,12);if($_.addon.length!=0){var _1n=$s(12+$_.addon.length+1);$P(_1n,0,$_.barcode);$P(_1n,12," ");$P(_1n,13,$_.addon);$_.barcode=_1n}$p($_.options,"addongap",$_.addongap);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_ean13();var _1u=$k[--$j];$_.args=_1u;if($_.includetext){if($_.ismntextxoffset==null){var _1y=$_.ismntxt.length==18?-1:-12;$_.ismntextxoffset=_1y}if($_.ismntextyoffset==null){$_.ismntextyoffset=$g($g($_.args,"bhs"),2)*72+3}if($has($_.args,"txt")){$_.txt=$g($_.args,"txt");$_.newtxt=$a($_.txt.length+1);$P($_.newtxt,0,$_.txt);$p($_.newtxt,$_.newtxt.length-1,$a([$_.ismntxt,$_.ismntextxoffset,$_.ismntextyoffset,$_.ismntextfont,$_.ismntextsize]));$p($_.args,"txt",$_.newtxt)}else{$p($_.args,"txt",$a([$a([$_.ismntxt,$_.ismntextxoffset,$_.ismntextyoffset,$_.ismntextfont,$_.ismntextsize])]))}}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_issn(){var $__=$_;$_=Object.assign({},$_,bwipp_issn.globals);$_.dontdraw=false;$_.includetext=false;$_.issntextfont="OCR-A";$_.issntextsize=8;$_.issntextxoffset=null;$_.issntextyoffset=null;$_.height=-1;$_.width=0;$_.addongap=12;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode," ");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.barcode=_6;$j--;var _7=$k[--$j];$_.seqvar=_7}else{$j--;$_.seqvar="00"}$X($_.seqvar," ");var _9=$k[--$j];if(_9){var _A=$k[--$j];$_.seqvar=_A;$j--;var _B=$k[--$j];$_.addon=_B}else{$j--;$_.addon=""}if($_.barcode.length!=8&&$_.barcode.length!=9){$k[$j++]="bwipp.issnBadLength#8636";$k[$j++]="ISSN must be 8 or 9 characters including dash, in the format XXXX-XXXX";bwipp_raiseerror()}var _F=$G($_.barcode,0,4);for(var _G=0,_H=_F.length;_G<_H;_G++){var _I=$g(_F,_G);if(_I<48||_I>57){$k[$j++]="bwipp.issnFirstThroughFourthNotNumeric#8640";$k[$j++]="ISSN first four characters must be numeral characters";bwipp_raiseerror()}}if($ne($G($_.barcode,4,1),"-")){$k[$j++]="bwipp.issnNeedsDash#8644";$k[$j++]="ISSN fifth character must be a dash";bwipp_raiseerror()}var _M=$G($_.barcode,5,3);for(var _N=0,_O=_M.length;_N<_O;_N++){var _P=$g(_M,_N);if(_P<48||_P>57){$k[$j++]="bwipp.issnSixthThroughEighthNotNumeric#8648";$k[$j++]="ISSN sixth through eighth characters must be numerals";bwipp_raiseerror()}}if($_.barcode.length==9){var _S=$g($_.barcode,8);if((_S<48||_S>57)&&_S!=88){$k[$j++]="bwipp.issnNinthCharacterBadFormat#8653";$k[$j++]="ISSN ninth character must be a number or the character X";bwipp_raiseerror()}}if($_.seqvar.length!=2){$k[$j++]="bwipp.issnBadSequenceVariantLength#8657";$k[$j++]="Sequence variant for ISSN must be 2 digits";bwipp_raiseerror()}$F($_.seqvar,function(){var _V=$k[--$j];if(_V<48||_V>57){$k[$j++]="bwipp.issnSequenceVariantBadCharacter#8661";$k[$j++]="Sequence variant for ISSN must contain only digits";bwipp_raiseerror()}});if($_.addon.length!=0&&$_.addon.length!=2&&$_.addon.length!=5){$k[$j++]="bwipp.issnBadAddOnLength#8665";$k[$j++]="Add-on for ISSN must be 2 or 5 digits";bwipp_raiseerror()}$_.issn=$s(8);$_.checksum=0;$_.i=0;$_.n=0;for(;;){$_.issnchar=$g($_.barcode,$_.i)-48;if($_.issnchar!=-3){$p($_.issn,$_.n,$_.issnchar+48);if($_.n<7){$_.checksum=$f($_.checksum+$_.issnchar*(8-$_.n))}$_.n=$_.n+1}$_.i=$_.i+1;if($_.i==$_.barcode.length){break}}$_.checksum=(11-$_.checksum%11)%11;var _r=$_.checksum+48;if(_r==58){_r=88}$_.checksum=_r;if($_.barcode.length==9){if($g($_.barcode,8)!=$_.checksum){$k[$j++]="bwipp.issnBadCheckDigit#8690";$k[$j++]="Incorrect ISSN check digit provided";bwipp_raiseerror()}}$_.pad=$s(14);$P($_.pad,0,"ISSN ");$P($_.pad,5,$_.barcode);$p($_.pad,13,$_.checksum);$_.issntxt=$_.pad;$_.barcode=$G($_.issn,0,7);$_.barcode=$s(12);$P($_.barcode,0,"977");$P($_.barcode,3,$_.issn);$P($_.barcode,10,$_.seqvar);if($_.addon.length!=0){var _1F=$s(12+$_.addon.length+1);$P(_1F,0,$_.barcode);$P(_1F,12," ");$P(_1F,13,$_.addon);$_.barcode=_1F}$p($_.options,"addongap",$_.addongap);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_ean13();var _1M=$k[--$j];$_.args=_1M;if($_.includetext){if($_.issntextxoffset==null){$_.issntextxoffset=10}if($_.issntextyoffset==null){$_.issntextyoffset=$g($g($_.args,"bhs"),2)*72+3}if($has($_.args,"txt")){$_.txt=$g($_.args,"txt");$_.newtxt=$a($_.txt.length+1);$P($_.newtxt,0,$_.txt);$p($_.newtxt,$_.newtxt.length-1,$a([$_.issntxt,$_.issntextxoffset,$_.issntextyoffset,$_.issntextfont,$_.issntextsize]));$p($_.args,"txt",$_.newtxt)}else{$p($_.args,"txt",$a([$a([$_.issntxt,$_.issntextxoffset,$_.issntextyoffset,$_.issntextfont,$_.issntextsize])]))}}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_mands(){var $__=$_;$_=Object.assign({},$_,bwipp_mands.globals);$_.dontdraw=false;$_.includetext=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.barlen=$_.barcode.length;if($_.barlen!=7&&$_.barlen!=8){$k[$j++]="bwipp.MandSbadLength#8828";$k[$j++]="M&S barcode must be 7 or 8 characters";bwipp_raiseerror()}if($_.barlen==7){var _9=$Z($s(8),"00000000");$P(_9,1,$_.barcode);$_.barcode=_9}$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_ean8();var _D=$k[--$j];$_.args=_D;var _F=$g($_.args,"bbs");$p(_F,10,$g(_F,2));$p(_F,11,$g(_F,2));var _J=$g($_.args,"bhs");$p(_J,10,$g(_J,2));$p(_J,11,$g(_J,2));if($_.includetext){$_.txt=$g($_.args,"txt");if($_.barlen==7){for(var _Q=0;_Q<=6;_Q+=1){var _R=$_.txt;$P($g($g(_R,_Q),0),0,$g($g(_R,_Q+1),0))}$P($g($g($_.txt,7),0),0," ")}var _Z=$a(10);$P(_Z,0,$_.txt);$_.txt=_Z;var _c=$g($_.txt,0);$p($_.txt,8,$A($a(_c.length),_c));var _h=$g($_.txt,0);$p($_.txt,9,$A($a(_h.length),_h));$p($g($_.txt,8),0,"M");$p($g($_.txt,8),1,-12);$p($g($_.txt,9),0,"S");$p($g($_.txt,9),1,69);$p($_.args,"txt",$_.txt)}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code128(){if(!bwipp_code128.globals){var $__=$_;$_=bwipp_code128.globals={};$_.code128_sta=-1;$_.code128_stb=-2;$_.code128_stc=-3;$_.code128_swa=-4;$_.code128_swb=-5;$_.code128_swc=-6;$_.code128_fn1=-7;$_.code128_fn2=-8;$_.code128_fn3=-9;$_.code128_fn4=-10;$_.code128_sft=-11;$_.code128_stp=-12;$_.code128_lka=-13;$_.code128_lkc=-14;$_.sta=$_.code128_sta;$_.stb=$_.code128_stb;$_.stc=$_.code128_stc;$_.swa=$_.code128_swa;$_.swb=$_.code128_swb;$_.swc=$_.code128_swc;$_.fn1=$_.code128_fn1;$_.fn2=$_.code128_fn2;$_.fn3=$_.code128_fn3;$_.fn4=$_.code128_fn4;$_.sft=$_.code128_sft;$_.stp=$_.code128_stp;$_.lka=$_.code128_lka;$_.lkc=$_.code128_lkc;$_.code128_charmaps=$a([$a([32,32,"00"]),$a(["!","!","01"]),$a(['"','"',"02"]),$a(["#","#","03"]),$a(["$","$","04"]),$a(["%","%","05"]),$a(["&","&","06"]),$a(["'","'","07"]),$a([40,40,"08"]),$a([41,41,"09"]),$a(["*","*","10"]),$a(["+","+","11"]),$a([",",",","12"]),$a(["-","-","13"]),$a([".",".","14"]),$a(["/","/","15"]),$a(["0","0","16"]),$a(["1","1","17"]),$a(["2","2","18"]),$a(["3","3","19"]),$a(["4","4","20"]),$a(["5","5","21"]),$a(["6","6","22"]),$a(["7","7","23"]),$a(["8","8","24"]),$a(["9","9","25"]),$a([":",":","26"]),$a([";",";","27"]),$a(["<","<","28"]),$a(["=","=","29"]),$a([">",">","30"]),$a(["?","?","31"]),$a(["@","@","32"]),$a(["A","A","33"]),$a(["B","B","34"]),$a(["C","C","35"]),$a(["D","D","36"]),$a(["E","E","37"]),$a(["F","F","38"]),$a(["G","G","39"]),$a(["H","H","40"]),$a(["I","I","41"]),$a(["J","J","42"]),$a(["K","K","43"]),$a(["L","L","44"]),$a(["M","M","45"]),$a(["N","N","46"]),$a(["O","O","47"]),$a(["P","P","48"]),$a(["Q","Q","49"]),$a(["R","R","50"]),$a(["S","S","51"]),$a(["T","T","52"]),$a(["U","U","53"]),$a(["V","V","54"]),$a(["W","W","55"]),$a(["X","X","56"]),$a(["Y","Y","57"]),$a(["Z","Z","58"]),$a(["[","[","59"]),$a([92,92,"60"]),$a(["]","]","61"]),$a(["^","^","62"]),$a(["_","_","63"]),$a([0,"`","64"]),$a([1,"a","65"]),$a([2,"b","66"]),$a([3,"c","67"]),$a([4,"d","68"]),$a([5,"e","69"]),$a([6,"f","70"]),$a([7,"g","71"]),$a([8,"h","72"]),$a([9,"i","73"]),$a([10,"j","74"]),$a([11,"k","75"]),$a([12,"l","76"]),$a([13,"m","77"]),$a([14,"n","78"]),$a([15,"o","79"]),$a([16,"p","80"]),$a([17,"q","81"]),$a([18,"r","82"]),$a([19,"s","83"]),$a([20,"t","84"]),$a([21,"u","85"]),$a([22,"v","86"]),$a([23,"w","87"]),$a([24,"x","88"]),$a([25,"y","89"]),$a([26,"z","90"]),$a([27,"{","91"]),$a([28,"|","92"]),$a([29,"}","93"]),$a([30,"~","94"]),$a([31,127,"95"]),$a([$_.fn3,$_.fn3,"96"]),$a([$_.fn2,$_.fn2,"97"]),$a([$_.sft,$_.sft,"98"]),$a([$_.swc,$_.swc,"99"]),$a([$_.swb,$_.fn4,$_.swb]),$a([$_.fn4,$_.swa,$_.swa]),$a([$_.fn1,$_.fn1,$_.fn1]),$a([$_.sta,$_.sta,$_.sta]),$a([$_.stb,$_.stb,$_.stb]),$a([$_.stc,$_.stc,$_.stc]),$a([$_.stp,$_.stp,$_.stp])]);$_.code128_latch_a0=$a(["","e","e","ee","eee","eee"]);$_.code128_latch_b0=$a(["d","","d","ddd","dd","ddd"]);$_.code128_latch_c0=$a(["c","c","","eec","ddc","dddc"]);$_.code128_latch_a1=$a(["ee","eee","eee","","e","e"]);$_.code128_latch_b1=$a(["ddd","dd","ddd","d","","d"]);$_.code128_latch_c1=$a(["eec","ddc","dddc","c","c",""]);var _2N=[];var _2J=$_.code128_latch_a0;for(var _2K=0,_2L=_2J.length;_2K<_2L;_2K++){_2N[_2K]=$g(_2J,_2K).length}_2N=$a(_2N);$_.code128_latch_length_a0=_2N;var _2S=[];var _2O=$_.code128_latch_b0;for(var _2P=0,_2Q=_2O.length;_2P<_2Q;_2P++){_2S[_2P]=$g(_2O,_2P).length}_2S=$a(_2S);$_.code128_latch_length_b0=_2S;var _2X=[];var _2T=$_.code128_latch_c0;for(var _2U=0,_2V=_2T.length;_2U<_2V;_2U++){_2X[_2U]=$g(_2T,_2U).length}_2X=$a(_2X);$_.code128_latch_length_c0=_2X;var _2c=[];var _2Y=$_.code128_latch_a1;for(var _2Z=0,_2a=_2Y.length;_2Z<_2a;_2Z++){_2c[_2Z]=$g(_2Y,_2Z).length}_2c=$a(_2c);$_.code128_latch_length_a1=_2c;var _2h=[];var _2d=$_.code128_latch_b1;for(var _2e=0,_2f=_2d.length;_2e<_2f;_2e++){_2h[_2e]=$g(_2d,_2e).length}_2h=$a(_2h);$_.code128_latch_length_b1=_2h;var _2m=[];var _2i=$_.code128_latch_c1;for(var _2j=0,_2k=_2i.length;_2j<_2k;_2j++){_2m[_2j]=$g(_2i,_2j).length}_2m=$a(_2m);$_.code128_latch_length_c1=_2m;$_.code128_latch_sequence=$a([$_.code128_latch_a0,$_.code128_latch_b0,$_.code128_latch_c0,$_.code128_latch_a1,$_.code128_latch_b1,$_.code128_latch_c1]);$_.code128_start_code=$a([103,104,105]);$_.code128_state_priority=$a([1,0,2,4,3,5]);$_.code128_start_state=$a([0,1,2,0,1,2]);$_.code128_start_length=$a([1,1,1,1,1,1]);var _34=[];var _2y=$a([5,4,3,2,1,0]);for(var _2z=0,_30=_2y.length;_2z<_30;_2z++){_34[_2z]=$g($_.code128_state_priority,$g(_2y,_2z))}_34=$a(_34);$_.code128_reverse_priority=_34;$k[$j++]=Infinity;var _35=$_.code128_reverse_priority;for(var _36=0,_37=_35.length;_36<_37;_36++){var _38=$g(_35,_36);$k[$j++]=_38;$k[$j++]=$g($_.code128_latch_length_a0,_38);$k[$j++]=_38}var _3B=$a();$_.code128_prioritized_latch_length_a0=_3B;$k[$j++]=Infinity;var _3C=$_.code128_reverse_priority;for(var _3D=0,_3E=_3C.length;_3D<_3E;_3D++){var _3F=$g(_3C,_3D);$k[$j++]=_3F;$k[$j++]=$g($_.code128_latch_length_a1,_3F);$k[$j++]=_3F}var _3I=$a();$_.code128_prioritized_latch_length_a1=_3I;$k[$j++]=Infinity;var _3J=$_.code128_reverse_priority;for(var _3K=0,_3L=_3J.length;_3K<_3L;_3K++){var _3M=$g(_3J,_3K);$k[$j++]=_3M;$k[$j++]=$g($_.code128_latch_length_b0,_3M);$k[$j++]=_3M}var _3P=$a();$_.code128_prioritized_latch_length_b0=_3P;$k[$j++]=Infinity;var _3Q=$_.code128_reverse_priority;for(var _3R=0,_3S=_3Q.length;_3R<_3S;_3R++){var _3T=$g(_3Q,_3R);$k[$j++]=_3T;$k[$j++]=$g($_.code128_latch_length_b1,_3T);$k[$j++]=_3T}var _3W=$a();$_.code128_prioritized_latch_length_b1=_3W;$k[$j++]=Infinity;var _3X=$_.code128_reverse_priority;for(var _3Y=0,_3Z=_3X.length;_3Y<_3Z;_3Y++){var _3a=$g(_3X,_3Y);$k[$j++]=_3a;$k[$j++]=$g($_.code128_latch_length_c0,_3a);$k[$j++]=_3a}var _3d=$a();$_.code128_prioritized_latch_length_c0=_3d;$k[$j++]=Infinity;var _3e=$_.code128_reverse_priority;for(var _3f=0,_3g=_3e.length;_3f<_3g;_3f++){var _3h=$g(_3e,_3f);$k[$j++]=_3h;$k[$j++]=$g($_.code128_latch_length_c1,_3h);$k[$j++]=_3h}var _3k=$a();$_.code128_prioritized_latch_length_c1=_3k;$k[$j++]=Infinity;$k[$j++]=$_.fn3;$k[$j++]=96;$k[$j++]=$_.fn2;$k[$j++]=97;$k[$j++]=$_.fn1;$k[$j++]=102;$k[$j++]=$_.stp;$k[$j++]=106;$k[$j++]=$_.lka;$k[$j++]=100;$k[$j++]=$_.lkc;$k[$j++]=99;var _3r=$d();$_.code128_seta_new=_3r;$k[$j++]=Infinity;$k[$j++]=$_.fn3;$k[$j++]=96;$k[$j++]=$_.fn2;$k[$j++]=97;$k[$j++]=$_.fn1;$k[$j++]=102;$k[$j++]=$_.stp;$k[$j++]=106;$k[$j++]=$_.lka;$k[$j++]=99;$k[$j++]=$_.lkc;$k[$j++]=101;var _3y=$d();$_.code128_setb_new=_3y;$k[$j++]=Infinity;$k[$j++]=$_.fn1;$k[$j++]=102;$k[$j++]=$_.stp;$k[$j++]=106;$k[$j++]=$_.lka;$k[$j++]=101;$k[$j++]=$_.lkc;$k[$j++]=100;var _43=$d();$_.code128_setc_new=_43;$_.code128_encs=$a(["212222","222122","222221","121223","121322","131222","122213","122312","132212","221213","221312","231212","112232","122132","122231","113222","123122","123221","223211","221132","221231","213212","223112","312131","311222","321122","321221","312212","322112","322211","212123","212321","232121","111323","131123","131321","112313","132113","132311","211313","231113","231311","112133","112331","132131","113123","113321","133121","313121","211331","231131","213113","213311","213131","311123","311321","331121","312113","312311","332111","314111","221411","431111","111224","111422","121124","121421","141122","141221","112214","112412","122114","122411","142112","142211","241211","221114","413111","241112","134111","111242","121142","121241","114212","124112","124211","411212","421112","421211","212141","214121","412121","111143","111341","131141","114113","114311","411113","411311","113141","114131","311141","411131","211412","211214","211232","2331112"]);$_.code128_max_int=2147483647;$_.charvals_legacy=$a([new Map,new Map,new Map]);for(var _46=0,_45=$_.code128_charmaps.length-1;_46<=_45;_46+=1){$_.i=_46;$_.encs=$g($_.code128_charmaps,$_.i);for(var _49=0;_49<=2;_49+=1){$_.j=_49;var _4C=$g($_.encs,$_.j);var _4D=$t(_4C);if($eq(_4D,"stringtype")){_4C=$g(_4C,0)}$p($g($_.charvals_legacy,$_.j),_4C,$_.i)}}$_.charvals_legacy=$_.charvals_legacy;var _4N=$g($_.charvals_legacy,0);$p(_4N,$_.code128_lka,$g(_4N,$_.code128_swb));$p(_4N,$_.code128_lkc,$g(_4N,$_.code128_swc));$_.seta_legacy=_4N;var _4R=$g($_.charvals_legacy,1);$p(_4R,$_.code128_lka,$g(_4R,$_.code128_swc));$p(_4R,$_.code128_lkc,$g(_4R,$_.code128_swa));$_.setb_legacy=_4R;var _4V=$g($_.charvals_legacy,2);$p(_4V,$_.code128_lka,$g(_4V,$_.code128_swa));$p(_4V,$_.code128_lkc,$g(_4V,$_.code128_swb));$_.setc_legacy=_4V;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code128.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.raw=false;$_.newencoder=false;$_.parse=false;$_.parsefnc=false;$_.suppressc=false;$_.unlatchextbeforec=false;$_.modunit=1;bwipp_processoptions();var _4Y=$k[--$j];$_.options=_4Y;var _4Z=$k[--$j];$_.barcode=_4Z;if($_.barcode.length>500){$k[$j++]="bwipp.code128inputTooLong#9131";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.code128_fn1],["FNC2",$_.code128_fn2],["FNC3",$_.code128_fn3],["LNKA",$_.code128_lka],["LNKC",$_.code128_lkc]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _4h=$k[--$j];$_.msg=_4h;$_.msglen=$_.msg.length;$_.encoding="legacy";if($_.newencoder){$_.encoding="new"}if($_.raw){$_.encoding="raw"}if($eq($_.encoding,"raw")){$_.cws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _4v=$G($_.barcode,$_.i+1,3);$k[$j++]=_4v;$k[$j++]=false;for(var _4w=0,_4x=_4v.length;_4w<_4x;_4w++){var _4y=$g(_4v,_4w);if(_4y<48||_4y>57){$k[$j-1]=true}}var _4z=$k[--$j];if(_4z){$j--;break}var _50=$k[--$j];$_.cw=$i(_50);if($_.cw>106){$k[$j++]="bwipp.code128BadRawCodeword#9178";$k[$j++]="Raw codewords must be 0 to 106";bwipp_raiseerror()}$p($_.cws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.code128BadRawFormat#9185";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.cws=$G($_.cws,0,$_.j);$_.text=""}else{$_.text=$s($_.msglen);for(var _5G=0,_5F=$_.msglen-1;_5G<=_5F;_5G+=1){$_.i=_5G;var _5L=$g($_.msg,$_.i);var _5O=$_.text;var _5N=$_.i;if(_5L<0){_5L=32}$p(_5O,_5N,_5L)}}if($eq($_.encoding,"legacy")){$_.seta=$_.seta_legacy;$_.setb=$_.setb_legacy;$_.setc=$_.setc_legacy;$k[$j++]=Infinity;for(var _5U=0,_5V=$_.msglen;_5U<_5V;_5U++){$k[$j++]=0}$k[$j++]=0;$_.numSA=$a();$k[$j++]=Infinity;for(var _5X=0,_5Y=$_.msglen;_5X<_5Y;_5X++){$k[$j++]=0}$k[$j++]=0;$_.numEA=$a();for(var _5a=$_.msglen-1;_5a>=0;_5a-=1){$_.i=_5a;if($g($_.msg,$_.i)>=0){if($g($_.msg,$_.i)>=128){$p($_.numEA,$_.i,$g($_.numEA,$_.i+1)+1)}else{$p($_.numSA,$_.i,$g($_.numSA,$_.i+1)+1)}}}$_.ea=false;$k[$j++]=Infinity;for(var _5t=0,_5s=$_.msglen-1;_5t<=_5s;_5t+=1){$_.i=_5t;$_.c=$g($_.msg,$_.i);if(!($_.ea!=$_.c<128)&&$_.c>=0){var _64=$_.ea?$_.numSA:$_.numEA;var _65=$g(_64,$_.i);var _68=$f(_65+$_.i)==$_.msglen?3:5;if(_65<_68){$k[$j++]=$_.code128_fn4}else{$_.ea=!$_.ea;$k[$j++]=$_.code128_fn4;$k[$j++]=$_.code128_fn4}}if($_.c>=0){$k[$j++]=$_.c&127}else{$k[$j++]=$_.c}}$as($a($m()));var _6F=$k[--$j];$_.msg=_6F;$j--;$_.msglen=$_.msg.length;$_.numsscr=function(){$_.s=0;var _6H=$k[--$j];$_.p=_6H;for(;;){if($_.p>=$_.msglen){break}var _6M=$g($_.msg,$_.p);var _6O=$has($_.setc,_6M);$k[$j++]=_6M;if(!_6O){$j--;break}var _6P=$k[$j-1];if(_6P<=-1){var _6Q=$k[--$j];if(_6Q==$_.code128_fn1&&$_.s%2==0){$_.s=$_.s+1}else{break}}else{$j--}$_.s=$_.s+1;$_.p=$_.p+1}$k[$j++]=$_.s};$_.enca=function(){var _6X=$k[--$j];$p($_.cws,$_.j,$g($_.seta,_6X));$_.j=$_.j+1};$_.encb=function(){var _6d=$k[--$j];$p($_.cws,$_.j,$g($_.setb,_6d));$_.j=$_.j+1};$_.encc=function(){var _6i=$k[$j-1];var _6j=$t(_6i);if($ne(_6j,"arraytype")){var _6l=$k[--$j];$k[$j++]=$g($_.setc,_6l)}else{var _6n=$k[--$j];$q(_6n);var _6o=$k[--$j];var _6p=$k[--$j];$k[$j++]=$f(_6o-48+(_6p-48)*10)}var _6r=$k[--$j];$p($_.cws,$_.j,_6r);$_.j=$_.j+1};$_.anotb=function(){var _6u=$k[--$j];$k[$j++]=$has($_.seta,_6u)&&!$has($_.setb,_6u)};$_.bnota=function(){var _6z=$k[--$j];$k[$j++]=$has($_.setb,_6z)&&!$has($_.seta,_6z)};$k[$j++]=Infinity;for(var _75=0,_76=$_.msg.length;_75<_76;_75++){$k[$j++]=0}$k[$j++]=9999;$_.nextanotb=$a();$k[$j++]=Infinity;for(var _78=0,_79=$_.msg.length;_78<_79;_78++){$k[$j++]=0}$k[$j++]=9999;$_.nextbnota=$a();for(var _7B=$_.msg.length-1;_7B>=0;_7B-=1){$_.i=_7B;$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _7F=$k[--$j];if(_7F){$p($_.nextanotb,$_.i,0)}else{$p($_.nextanotb,$_.i,$g($_.nextanotb,$_.i+1)+1)}$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _7Q=$k[--$j];if(_7Q){$p($_.nextbnota,$_.i,0)}else{$p($_.nextbnota,$_.i,$g($_.nextbnota,$_.i+1)+1)}}$_.abeforeb=function(){var _7Y=$k[--$j];$k[$j++]=$lt($g($_.nextanotb,_7Y),$g($_.nextbnota,_7Y))};$_.bbeforea=function(){var _7d=$k[--$j];$k[$j++]=$lt($g($_.nextbnota,_7d),$g($_.nextanotb,_7d))};$_.cws=$a($_.barcode.length*2+3);$_.j=0;if($_.msglen>0){$k[$j++]=0;$_.numsscr()}else{$k[$j++]=-1}var _7l=$k[--$j];$_.nums=_7l;for(;;){if($_.msglen==0){$k[$j++]=$_.code128_stb;$_.enca();$_.cset="setb";break}if($_.msglen==2&&$_.nums==2){$k[$j++]=$_.code128_stc;$_.enca();$_.cset="setc";break}if($_.nums>=4){$k[$j++]=$_.code128_stc;$_.enca();$_.cset="setc";break}$k[$j++]=0;$_.abeforeb();var _7q=$k[--$j];if(_7q){$k[$j++]=$_.code128_sta;$_.enca();$_.cset="seta";break}$k[$j++]=$_.code128_stb;$_.enca();$_.cset="setb";break}$_.i=0;for(;;){if($_.i==$_.msglen){break}$k[$j++]=$_.i;$_.numsscr();var _7u=$k[--$j];$_.nums=_7u;for(;;){if(($eq($_.cset,"seta")||$eq($_.cset,"setb"))&&$_.nums>=4&&$g($_.msg,$_.i)!=$_.code128_fn1){if($_.nums%2==0){$k[$j++]=$_.code128_swc;if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.cset="setc";break}else{$k[$j++]=$g($_.msg,$_.i);if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.i=$_.i+1;$k[$j++]=$_.i;$_.numsscr();var _89=$k[--$j];if(_89>=4){$k[$j++]=$_.code128_swc;if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.cset="setc";break}}}$k[$j++]=$eq($_.cset,"setb");$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _8F=$k[--$j];var _8G=$k[--$j];if(_8G&&_8F){if($_.i<$_.msglen-1){$k[$j++]=$_.i+1;$_.bbeforea();var _8K=$k[--$j];if(_8K){$k[$j++]=$_.code128_sft;$_.encb();$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}}$k[$j++]=$_.code128_swa;$_.encb();$_.cset="seta";break}$k[$j++]=$eq($_.cset,"seta");$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _8T=$k[--$j];var _8U=$k[--$j];if(_8U&&_8T){if($_.i<$_.msglen-1){$k[$j++]=$_.i+1;$_.abeforeb();var _8Y=$k[--$j];if(_8Y){$k[$j++]=$_.code128_sft;$_.enca();$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}}$k[$j++]=$_.code128_swb;$_.enca();$_.cset="setb";break}if($eq($_.cset,"setc")&&$_.nums<2&&($g($_.msg,$_.i)>-1||$g($_.msg,$_.i)==$_.code128_fn4)){$k[$j++]=$_.i;$_.abeforeb();var _8m=$k[--$j];if(_8m){$k[$j++]=$_.code128_swa;$_.encc();$_.cset="seta";break}$k[$j++]=$_.code128_swb;$_.encc();$_.cset="setb";break}if($eq($_.cset,"seta")){$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}if($eq($_.cset,"setb")){$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}if($eq($_.cset,"setc")){if($g($_.msg,$_.i)<=-1){$k[$j++]=$g($_.msg,$_.i);$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}break}break}}$_.cws=$G($_.cws,0,$_.j)}if($eq($_.encoding,"new")){$_.seta=$_.code128_seta_new;$_.setb=$_.code128_setb_new;$_.setc=$_.code128_setc_new;$_.encode=$a([function(){$_.enc_a0()},function(){$_.enc_b0()},function(){$_.enc_c()},function(){$_.enc_a1()},function(){$_.enc_b1()},function(){$_.enc_c()}]);$_.can_a=function(){if($_.c>=0){$k[$j++]=true}else{$k[$j++]=$has($_.seta,$_.c)}};$_.can_b=function(){if($_.c>=0){$k[$j++]=true}else{$k[$j++]=$has($_.setb,$_.c)}};$_.can_c0=function(){if($_.num_digits>=2){$k[$j++]=true}else{$k[$j++]=$has($_.setc,$_.c)}};$_.can_c1=function(){if($_.num_digits>=2){$k[$j++]=true}else{$k[$j++]=$has($_.setc,$_.c)}};if($_.suppressc){$_.can_c0=function(){$k[$j++]=false}}if($_.suppressc||$_.unlatchextbeforec){$_.can_c1=function(){$k[$j++]=false}}$_.out_a0=function(){$k[$j++]=1;if($_.c>=0){if($_.c>=128){$k[$j-1]+=1}if(($_.c&127)>=96){$k[$j-1]+=1}}};$_.out_a1=function(){$k[$j++]=1;if($_.c>=0){if($_.c<128){$k[$j-1]+=1}if(($_.c&127)>=96){$k[$j-1]+=1}}};$_.out_b0=function(){$k[$j++]=1;if($_.c>=0){if($_.c>=128){$k[$j-1]+=1}if(($_.c&127)<32){$k[$j-1]+=1}}};$_.out_b1=function(){$k[$j++]=1;if($_.c>=0){if($_.c<128){$k[$j-1]+=1}if(($_.c&127)<32){$k[$j-1]+=1}}};$_.map_ab=function(){var _9q=$k[$j-1];if(_9q<32){$k[$j-1]+=64}else{$k[$j-1]-=32}};$_.enc_a0=function(){$k[$j++]=Infinity;if($_.c<0){$k[$j++]=$g($_.seta,$_.c)}else{if($_.c>=128){$k[$j++]=101}var _9z=$_.c&127;$k[$j++]=_9z;if(_9z>=96){var _A0=$k[--$j];$k[$j++]=98;$k[$j++]=_A0}$_.map_ab()}var _A1=$a();$k[$j++]=_A1};$_.enc_a1=function(){$k[$j++]=Infinity;if($_.c<0){$k[$j++]=$g($_.seta,$_.c)}else{if($_.c<128){$k[$j++]=101}var _A8=$_.c&127;$k[$j++]=_A8;if(_A8>=96){var _A9=$k[--$j];$k[$j++]=98;$k[$j++]=_A9}$_.map_ab()}var _AA=$a();$k[$j++]=_AA};$_.enc_b0=function(){$k[$j++]=Infinity;if($_.c<0){$k[$j++]=$g($_.setb,$_.c)}else{if($_.c>=128){$k[$j++]=100}var _AH=$_.c&127;$k[$j++]=_AH;if(_AH<32){var _AI=$k[--$j];$k[$j++]=98;$k[$j++]=_AI}$_.map_ab()}var _AJ=$a();$k[$j++]=_AJ};$_.enc_b1=function(){$k[$j++]=Infinity;if($_.c<0){$k[$j++]=$g($_.setb,$_.c)}else{if($_.c<128){$k[$j++]=100}var _AQ=$_.c&127;$k[$j++]=_AQ;if(_AQ<32){var _AR=$k[--$j];$k[$j++]=98;$k[$j++]=_AR}$_.map_ab()}var _AS=$a();$k[$j++]=_AS};$_.enc_c=function(){$k[$j++]=Infinity;if($_.c<0){$k[$j++]=$g($_.setc,$_.c)}else{$k[$j++]=$f(($g($_.msg,$_.n)-48)*10+($g($_.msg,$_.n+1)-48))}var _Ad=$a();$k[$j++]=_Ad};$_.get_best_prior_state=function(){var _Af=$k[--$j];var _Ah=$k[--$j];$_.len=$f(_Ah+$g($_.bln_0,_Af));var _Ai=$k[--$j];$_.o=_Ai;var _Ak=$k[--$j];var _Am=$k[--$j];var _An=$f(_Am+$g($_.bln_0,_Ak));$k[$j++]=_An;if(_An<$_.len){var _Ap=$k[--$j];$_.len=_Ap;var _Aq=$k[--$j];$_.o=_Aq}else{$j-=2}var _As=$k[--$j];var _Au=$k[--$j];var _Av=$f(_Au+$g($_.bln_0,_As));$k[$j++]=_Av;if(_Av<$_.len){var _Ax=$k[--$j];$_.len=_Ax;var _Ay=$k[--$j];$_.o=_Ay}else{$j-=2}var _B0=$k[--$j];var _B2=$k[--$j];var _B3=$f(_B2+$g($_.bln_0,_B0));$k[$j++]=_B3;if(_B3<$_.len){var _B5=$k[--$j];$_.len=_B5;var _B6=$k[--$j];$_.o=_B6}else{$j-=2}var _B8=$k[--$j];var _BA=$k[--$j];var _BB=$f(_BA+$g($_.bln_0,_B8));$k[$j++]=_BB;if(_BB<$_.len){var _BD=$k[--$j];$_.len=_BD;var _BE=$k[--$j];$_.o=_BE}else{$j-=2}var _BG=$k[--$j];var _BI=$k[--$j];if($f(_BI+$g($_.bln_0,_BG))<$_.len){var _BK=$k[--$j];$_.o=_BK}else{$j--}$k[$j++]=$_.o};$_.bln_0=$_.code128_start_length;$_.bln_1=$_.code128_start_length;$k[$j++]=Infinity;for(var _BM=0;_BM<4;_BM++){$k[$j++]=$a([0,0,0,0,0,0])}$_.bln=$a();$_.bps_0=$_.code128_start_state;$_.bps_1=$_.code128_start_state;$k[$j++]=Infinity;for(var _BO=0;_BO<4;_BO++){$k[$j++]=$a([0,0,0,0,0,0])}$_.bps=$a();$k[$j++]=Infinity;for(var _BR=0,_BS=$_.msg.length;_BR<_BS;_BR++){$k[$j++]=$a([0,0,0,0,0,0])}$_.path=$a();$_.make_tables=function(){$_.num_digits=0;for(var _BW=0,_BV=$_.msg.length-1;_BW<=_BV;_BW+=1){$_.n=_BW;$_.c=$g($_.msg,$_.n);var _Bd=$_.c>=48&&$_.c<58?$_.num_digits+1:0;$_.num_digits=_Bd;$_.bln_2=$_.bln_1;$_.bln_1=$_.bln_0;$_.bln_0=$g($_.bln,$_.n&3);$_.bps_2=$_.bps_1;$_.bps_1=$_.bps_0;$_.bps_0=$g($_.bps,$_.n&3);var _Bs=$_.num_digits>=2?$_.bps_2:$_.bps_1;$_.bps_c=_Bs;var _Bx=$_.num_digits>=2?$_.bln_2:$_.bln_1;$_.bln_c=_Bx;$k[$j++]=$_.bln_0;$k[$j++]=0;$_.can_a();var _C0=$k[--$j];if(_C0){$_.p=$g($_.bps_1,0);$p($g($_.path,$_.n),0,$_.p);$k[$j++]=$f($g($_.bln_1,$_.p)+$g($_.code128_latch_length_a0,$_.p));$_.out_a0();var _CC=$k[--$j];var _CD=$k[--$j];$k[$j++]=$f(_CD+_CC)}else{$k[$j++]=$_.code128_max_int}var _CE=$k[--$j];var _CF=$k[--$j];var _CG=$k[--$j];$p(_CG,_CF,_CE);$k[$j++]=$_.bln_0;$k[$j++]=3;$_.can_a();var _CI=$k[--$j];if(_CI){$_.p=$g($_.bps_1,3);$p($g($_.path,$_.n),3,$_.p);$k[$j++]=$f($g($_.bln_1,$_.p)+$g($_.code128_latch_length_a1,$_.p));$_.out_a1();var _CU=$k[--$j];var _CV=$k[--$j];$k[$j++]=$f(_CV+_CU)}else{$k[$j++]=$_.code128_max_int}var _CW=$k[--$j];var _CX=$k[--$j];var _CY=$k[--$j];$p(_CY,_CX,_CW);$k[$j++]=$_.bln_0;$k[$j++]=1;$_.can_b();var _Ca=$k[--$j];if(_Ca){$_.p=$g($_.bps_1,1);$p($g($_.path,$_.n),1,$_.p);$k[$j++]=$f($g($_.bln_1,$_.p)+$g($_.code128_latch_length_b0,$_.p));$_.out_b0();var _Cm=$k[--$j];var _Cn=$k[--$j];$k[$j++]=$f(_Cn+_Cm)}else{$k[$j++]=$_.code128_max_int}var _Co=$k[--$j];var _Cp=$k[--$j];var _Cq=$k[--$j];$p(_Cq,_Cp,_Co);$k[$j++]=$_.bln_0;$k[$j++]=4;$_.can_b();var _Cs=$k[--$j];if(_Cs){$_.p=$g($_.bps_1,4);$p($g($_.path,$_.n),4,$_.p);$k[$j++]=$f($g($_.bln_1,$_.p)+$g($_.code128_latch_length_b1,$_.p));$_.out_b1();var _D4=$k[--$j];var _D5=$k[--$j];$k[$j++]=$f(_D5+_D4)}else{$k[$j++]=$_.code128_max_int}var _D6=$k[--$j];var _D7=$k[--$j];var _D8=$k[--$j];$p(_D8,_D7,_D6);$k[$j++]=$_.bln_0;$k[$j++]=2;$_.can_c0();var _DA=$k[--$j];if(_DA){$_.p=$g($_.bps_c,2);$p($g($_.path,$_.n),2,$_.p);$k[$j++]=$f($g($_.bln_c,$_.p)+$g($_.code128_latch_length_c0,$_.p))+1}else{$k[$j++]=$_.code128_max_int}var _DM=$k[--$j];var _DN=$k[--$j];var _DO=$k[--$j];$p(_DO,_DN,_DM);$k[$j++]=$_.bln_0;$k[$j++]=5;$_.can_c1();var _DQ=$k[--$j];if(_DQ){$_.p=$g($_.bps_c,5);$p($g($_.path,$_.n),5,$_.p);$k[$j++]=$f($g($_.bln_c,$_.p)+$g($_.code128_latch_length_c1,$_.p))+1}else{$k[$j++]=$_.code128_max_int}var _Dc=$k[--$j];var _Dd=$k[--$j];var _De=$k[--$j];$p(_De,_Dd,_Dc);$k[$j++]=$_.bps_0;$k[$j++]=0;$q($_.code128_prioritized_latch_length_a0);$_.get_best_prior_state();var _Dg=$k[--$j];var _Dh=$k[--$j];var _Di=$k[--$j];$p(_Di,_Dh,_Dg);$k[$j++]=$_.bps_0;$k[$j++]=3;$q($_.code128_prioritized_latch_length_a1);$_.get_best_prior_state();var _Dk=$k[--$j];var _Dl=$k[--$j];var _Dm=$k[--$j];$p(_Dm,_Dl,_Dk);$k[$j++]=$_.bps_0;$k[$j++]=1;$q($_.code128_prioritized_latch_length_b0);$_.get_best_prior_state();var _Do=$k[--$j];var _Dp=$k[--$j];var _Dq=$k[--$j];$p(_Dq,_Dp,_Do);$k[$j++]=$_.bps_0;$k[$j++]=4;$q($_.code128_prioritized_latch_length_b1);$_.get_best_prior_state();var _Ds=$k[--$j];var _Dt=$k[--$j];var _Du=$k[--$j];$p(_Du,_Dt,_Ds);$k[$j++]=$_.bps_0;$k[$j++]=2;$q($_.code128_prioritized_latch_length_c0);$_.get_best_prior_state();var _Dw=$k[--$j];var _Dx=$k[--$j];var _Dy=$k[--$j];$p(_Dy,_Dx,_Dw);$k[$j++]=$_.bps_0;$k[$j++]=5;$q($_.code128_prioritized_latch_length_c1);$_.get_best_prior_state();var _E0=$k[--$j];var _E1=$k[--$j];var _E2=$k[--$j];$p(_E2,_E1,_E0)}};$_.backtrack=function(){$_.n=$_.msg.length;for(var _E4=0,_E5=$_.code128_reverse_priority.length;_E4<_E5;_E4++){var _E6=$g($_.code128_reverse_priority,_E4);var _E8=$g($_.bln_0,_E6);$k[$j++]=_E6;$k[$j++]=_E8;$k[$j++]=_E8}$j--;var _E9=$k[--$j];$_.len=_E9;var _EA=$k[--$j];$_.state=_EA;for(var _EB=0;_EB<5;_EB++){var _ED=$k[--$j];if($lt(_ED,$_.len)){var _EE=$k[--$j];$_.len=_EE;var _EF=$k[--$j];$_.state=_EF}else{$j-=2}}$k[$j++]=$a($_.len);for(;;){if($_.n<=0){break}$_.prior_state=$g($g($_.path,$_.n-1),$_.state);$k[$j++]=Infinity;$F($g($g($_.code128_latch_sequence,$_.state),$_.prior_state));$_.latch=$a();var _EY=($_.state==2||$_.state==5)&&$g($_.msg,$_.n-1)>=48?2:1;$_.n=$_.n-_EY;$_.c=$g($_.msg,$_.n);$k[$j++]="enc";if($g($_.encode,$_.state)()===true){break}var _Ef=$k[--$j];var _Eg=$k[--$j];$_[_Eg]=_Ef;$_.len=$f($_.len-$_.latch.length-$_.enc.length);var _Ek=$k[--$j];var _El=$_.len;var _Em=$_.latch;$P(_Ek,_El,_Em);$P(_Ek,$f(_El+_Em.length),$_.enc);$_.state=$_.prior_state;$k[$j++]=_Ek}var _Ep=$k[$j-1];$p(_Ep,0,$g($_.code128_start_code,$_.state))};$_.make_tables();$_.backtrack();var _Es=$k[--$j];$_.cws=_Es}$_.j=$_.cws.length;var _Ev=$a($_.j+2);$P(_Ev,0,$_.cws);$_.cws=_Ev;$_.csum=$g($_.cws,0);for(var _F1=1,_F0=$_.j-1;_F1<=_F0;_F1+=1){$_.i=_F1;$_.csum=$f($_.csum+$g($_.cws,$_.i)*$_.i)}$_.csum=$_.csum%103;$p($_.cws,$_.j,$_.csum);$p($_.cws,$_.j+1,106);if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#9609";$k[$j++]=$_.cws;bwipp_raiseerror()}$_.sbs=$s($_.cws.length*6+1);for(var _FK=0,_FJ=$_.cws.length-1;_FK<=_FJ;_FK+=1){$_.i=_FK;$P($_.sbs,$_.i*6,$g($_.code128_encs,$g($_.cws,$_.i)))}$k[$j++]=Infinity;var _FR=$_.sbs;for(var _FS=0,_FT=_FR.length;_FS<_FT;_FS++){$k[$j++]=$g(_FR,_FS)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _FW=$_.sbs;var _Fb=[];for(var _FY=0,_FZ=~~(($_.sbs.length+1)/2);_FY<_FZ;_FY++){_Fb[_FY]=$_.height}_Fb=$a(_Fb);var _Ff=[];for(var _Fd=0,_Fe=~~(($_.sbs.length+1)/2);_Fd<_Fe;_Fd++){_Ff[_Fd]=0}_Ff=$a(_Ff);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_FW;$k[$j++]="bhs";$k[$j++]=_Fb;$k[$j++]="bbs";$k[$j++]=_Ff;$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="opt";$k[$j++]=$_.options;var _Fo=$d();$k[$j++]=_Fo;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_gs1_128(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1_128.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.linkagea=false;$_.linkagec=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=false;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;if($_.barcode.length>500){$k[$j++]="bwipp.gs1128inputTooLong#9714";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.linkagea&&$_.linkagec){$k[$j++]="bwipp.gs1128badLinkage#9723";$k[$j++]="linkagea and linkagec cannot be used together";bwipp_raiseerror()}$_.text=$_.barcode;$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _9=$k[--$j];$_.fncs=_9;var _A=$k[--$j];$_.vals=_A;var _B=$k[--$j];$_.ais=_B;$_.fnc1=-1;$k[$j++]=Infinity;$k[$j++]=$_.fnc1;for(var _F=0,_E=$_.ais.length-1;_F<=_E;_F+=1){$_.i=_F;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){$k[$j++]=$_.fnc1}}$as($a($m()));var _U=$k[--$j];$_.c128=_U;$j--;$_.barcode=$s(($_.c128.length+1)*5);$_.j=0;$F($_.c128,function(){var _Y=$k[$j-1];if(_Y==$_.fnc1){$j--;$P($_.barcode,$_.j,"^FNC1");$_.j=$_.j+5}else{var _e=$k[--$j];$p($_.barcode,$_.j,_e);$_.j=$_.j+1}});if($_.linkagea||$_.linkagec){var _o=$_.barcode;var _n=$_.j;var _m=$_.linkagea?"^LNKA":"^LNKC";$P(_o,_n,_m);$_.j=$_.j+5}$_.barcode=$G($_.barcode,0,$_.j);if($_.height==-1){if(!($_.propspec&&$_.hnom!=-1)){$_.height=.5}}$del($_.options,"parse");$p($_.options,"height",$_.height);$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code128();var _12=$k[--$j];$_.args=_12;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_ean14(){var $__=$_;$_=Object.assign({},$_,bwipp_ean14.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.text=$_.barcode;var _5=$_.barcode;$k[$j++]=$s(_5.length);$k[$j++]=0;$F(_5,function(){var _7=$k[$j-1];if(_7!=32){var _8=$k[--$j];var _9=$k[--$j];var _A=$k[$j-1];$p(_A,_9,_8);$k[$j++]=_9+1}else{$j--}});var _B=$k[--$j];var _C=$k[--$j];$_.barcode=$G(_C,0,_B);$_.hasspace=$_.text.length!=$_.barcode.length;if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.ean14badLength#9869";$k[$j++]="EAN-14 must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.ean14badAI#9872";$k[$j++]="EAN-14 must begin with (01) application identifier";bwipp_raiseerror()}var _M=$G($_.barcode,4,$_.barcode.length-4);for(var _N=0,_O=_M.length;_N<_O;_N++){var _P=$g(_M,_N);if(_P<48||_P>57){$k[$j++]="bwipp.ean14badCharacter#9876";$k[$j++]="EAN-14 must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _Q=0;_Q<=12;_Q+=1){$_.i=_Q;var _Y=$_.checksum;var _X=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_X=_X*3}$_.checksum=$f(_Y+_X)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.ean14badCheckDigit#9891";$k[$j++]="Incorrect EAN-14 check digit provided";bwipp_raiseerror()}}else{var _f=$s(18);$P(_f,0,$_.barcode);$p(_f,17,$_.checksum+48);$_.barcode=_f;var _k=$_.hasspace?2:1;var _l=$s($_.text.length+_k);$P(_l,_l.length-2," ");$p(_l,_l.length-1,$_.checksum+48);$P(_l,0,$_.text);$_.text=_l}$_.gtin=$G($_.barcode,4,14);$_.barcode=$s(21);$P($_.barcode,0,"^FNC101");$P($_.barcode,7,$_.gtin);$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code128();var _x=$k[--$j];$_.args=_x;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_sscc18(){var $__=$_;$_=Object.assign({},$_,bwipp_sscc18.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.text=$_.barcode;var _5=$_.barcode;$k[$j++]=$s(_5.length);$k[$j++]=0;$F(_5,function(){var _7=$k[$j-1];if(_7!=32){var _8=$k[--$j];var _9=$k[--$j];var _A=$k[$j-1];$p(_A,_9,_8);$k[$j++]=_9+1}else{$j--}});var _B=$k[--$j];var _C=$k[--$j];$_.barcode=$G(_C,0,_B);$_.hasspace=$_.text.length!=$_.barcode.length;if($_.barcode.length!=21&&$_.barcode.length!=22){$k[$j++]="bwipp.sscc18badLength#9994";$k[$j++]="SSCC-18 must be 17 or 18 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(00)")){$k[$j++]="bwipp.sscc18badAI#9997";$k[$j++]="SSCC-18 must begin with (00) application identifier";bwipp_raiseerror()}var _M=$G($_.barcode,4,$_.barcode.length-4);for(var _N=0,_O=_M.length;_N<_O;_N++){var _P=$g(_M,_N);if(_P<48||_P>57){$k[$j++]="bwipp.sscc18badCharacter#10001";$k[$j++]="SSCC-18 must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _Q=0;_Q<=16;_Q+=1){$_.i=_Q;var _Y=$_.checksum;var _X=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_X=_X*3}$_.checksum=$f(_Y+_X)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==22){if($g($_.barcode,21)!=$_.checksum+48){$k[$j++]="bwipp.sscc18badCheckDigit#10016";$k[$j++]="Incorrect SSCC-18 check digit provided";bwipp_raiseerror()}}else{var _f=$s(22);$P(_f,0,$_.barcode);$p(_f,21,$_.checksum+48);$_.barcode=_f;var _k=$_.hasspace?2:1;var _l=$s($_.text.length+_k);$P(_l,_l.length-2," ");$p(_l,_l.length-1,$_.checksum+48);$P(_l,0,$_.text);$_.text=_l}$_.sscc=$G($_.barcode,4,18);$_.barcode=$s(25);$P($_.barcode,0,"^FNC100");$P($_.barcode,7,$_.sscc);$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code128();var _x=$k[--$j];$_.args=_x;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code39(){if(!bwipp_code39.globals){var $__=$_;$_=bwipp_code39.globals={};$_.code39_encs=$a(["1113313111","3113111131","1133111131","3133111111","1113311131","3113311111","1133311111","1113113131","3113113111","1133113111","3111131131","1131131131","3131131111","1111331131","3111331111","1131331111","1111133131","3111133111","1131133111","1111333111","3111111331","1131111331","3131111311","1111311331","3111311311","1131311311","1111113331","3111113311","1131113311","1111313311","3311111131","1331111131","3331111111","1311311131","3311311111","1331311111","1311113131","3311113111","1331113111","1313131111","1313111311","1311131311","1113131311","1311313111"]);$_.code39_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _1=0;_1<=42;_1+=1){$p($_.charvals,$G($_.code39_barchars,_1,1),_1)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code39.globals);$_.dontdraw=false;$_.includecheck=false;$_.validatecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.hidestars=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _5=$k[--$j];$_.options=_5;var _6=$k[--$j];$_.barcode=_6;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.code39emptyData#10168";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.code39inputTooLong#10172";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _D=0,_C=$_.barcode.length-1;_D<=_C;_D+=1){if(!$has($_.charvals,$G($_.barcode,_D,1))){$k[$j++]="bwipp.code39badCharacter#10180";$k[$j++]="Code 39 must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _L=$_.barcode.length;if($_.validatecheck){_L=_L-1}$_.barlen=_L;$_.checksum=0;for(var _P=0,_O=$_.barlen-1;_P<=_O;_P+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_P,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.code39_barchars,$_.checksum)){$k[$j++]="bwipp.code39badCheckDigit#10200";$k[$j++]="Incorrect Code 39 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen);$_.includecheck=true}var _h=$_.includecheck?3:2;$_.sbs=$s($f($_.barlen+_h)*10);$P($_.sbs,0,$g($_.code39_encs,43));for(var _n=0,_m=$_.barlen-1;_n<=_m;_n+=1){$_.i=_n;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.sbs,$_.i*10+10,$g($_.code39_encs,$_.indx))}if($_.includecheck){$P($_.sbs,$_.barlen*10+10,$g($_.code39_encs,$_.checksum));$P($_.sbs,$_.barlen*10+20,$g($_.code39_encs,43))}else{$P($_.sbs,$_.barlen*10+10,$g($_.code39_encs,43))}var _19=!$_.hidestars?2:0;var _1E=$f(_19+$_.barlen);if($_.includecheck&&$_.includecheckintext){_1E=_1E+1}$_.slen=_1E;$_.text=$s($_.slen);$_.tpos=0;if(!$_.hidestars){$P($_.text,$_.tpos,"*");$_.tpos=$_.tpos+1}$P($_.text,$_.tpos,$_.barcode);$_.tpos=$f($_.tpos+$_.barlen);if($_.includecheck&&$_.includecheckintext){$P($_.text,$_.tpos,$G($_.code39_barchars,$_.checksum,1));$_.tpos=$_.tpos+1}if(!$_.hidestars){$P($_.text,$_.tpos,"*")}$k[$j++]=Infinity;var _1b=$_.sbs;for(var _1c=0,_1d=_1b.length;_1c<_1d;_1c++){$k[$j++]=$g(_1b,_1c)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _1g=$_.sbs;var _1l=[];for(var _1i=0,_1j=~~(($_.sbs.length+1)/2);_1i<_1j;_1i++){_1l[_1i]=$_.height}_1l=$a(_1l);var _1p=[];for(var _1n=0,_1o=~~(($_.sbs.length+1)/2);_1n<_1o;_1n++){_1p[_1n]=0}_1p=$a(_1p);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1g;$k[$j++]="bhs";$k[$j++]=_1l;$k[$j++]="bbs";$k[$j++]=_1p;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _1x=$d();$k[$j++]=_1x;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code39ext(){if(!bwipp_code39ext.globals){var $__=$_;$_=bwipp_code39ext.globals={};$_.code39ext_extencs=$a(["%U","$A","$B","$C","$D","$E","$F","$G","$H","$I","$J","$K","$L","$M","$N","$O","$P","$Q","$R","$S","$T","$U","$V","$W","$X","$Y","$Z","%A","%B","%C","%D","%E"," ","/A","/B","/C","/D","/E","/F","/G","/H","/I","/J","/K","/L","-",".","/O","0","1","2","3","4","5","6","7","8","9","/Z","%F","%G","%H","%I","%J","%V","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","%K","%L","%M","%N","%O","%W","+A","+B","+C","+D","+E","+F","+G","+H","+I","+J","+K","+L","+M","+N","+O","+P","+Q","+R","+S","+T","+U","+V","+W","+X","+Y","+Z","%P","%Q","%R","%S","%T"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code39ext.globals);$_.dontdraw=false;$_.includetext=false;$_.hidestars=false;$_.parse=false;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.code39extinputTooLong#10327";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _9=$k[--$j];$_.barcode=_9;$_.barlen=$_.barcode.length;$del($_.options,"parse");$F($_.barcode,function(){var _D=$k[--$j];if(_D>=128){$k[$j++]="bwipp.code39extBadCharacter#10354";$k[$j++]="Code 39 Extended must contain only ASCII characters";bwipp_raiseerror()}});$_.newcode=$s($_.barlen*2);$_.newtext=$s($_.barlen*2);$_.j=0;for(var _K=0,_J=$_.barlen-1;_K<=_J;_K+=1){$_.i=_K;$_.extchar=$g($_.code39ext_extencs,$g($_.barcode,$_.i));$P($_.newcode,$_.j,$_.extchar);var _W=$g($_.barcode,$_.i);var _Z=$_.newtext;var _Y=$_.j;if(_W<32||_W==127){_W=32}$p(_Z,_Y,_W);if($_.extchar.length!=1){$p($_.newtext,$_.j+1,32)}$_.j=$_.j+$_.extchar.length}$_.newcode=$G($_.newcode,0,$_.j);$_.newtext=$G($_.newtext,0,$_.j);$_.bwipp_dontdraw=true;$k[$j++]=$_.newcode;$k[$j++]=$_.options;bwipp_code39();var _n=$k[--$j];$_.args=_n;if($_.includetext){$_.txt=$g($_.args,"txt");$_.txtstr=$g($g($_.txt,0),0);var _w=$_.hidestars?0:1;$P($_.txtstr,_w,$_.newtext);$p($_.args,"txt",$_.txt)}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code32(){var $__=$_;$_=Object.assign({},$_,bwipp_code32.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.text1xoffset=0;$_.height=-1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=8&&$_.barcode.length!=9){$k[$j++]="bwipp.code32badLength#10449";$k[$j++]="Italian Pharmacode must be 8 or 9 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.code32badCharacter#10453";$k[$j++]="Italian Pharmacode must contain only digits";bwipp_raiseerror()}});$_.checksum=0;for(var _8=0;_8<=7;_8+=1){$_.i=_8;$k[$j++]=$g($_.barcode,$_.i)-48;if($_.i%2!=0){var _D=$k[--$j];$k[$j++]=_D*2}var _E=$k[$j-1];if(_E>9){$k[$j-1]-=9}var _H=$k[--$j];$_.checksum=$f(_H+$_.checksum)}$_.checksum=$_.checksum%10;if($_.barcode.length==9){if($g($_.barcode,8)!=$_.checksum+48){$k[$j++]="bwipp.code32badCheckDigit#10469";$k[$j++]="Incorrect Italian Pharmacode check digit provided";bwipp_raiseerror()}}var _N=$s(10);$P(_N,0,$_.barcode);$p(_N,8,$_.checksum+48);$_.text=_N;$_.val=$R($s(6),$i($_.text),32);$_.barcode=$s(6);for(var _U=0;_U<=5;_U+=1){$p($_.barcode,_U,48)}$P($_.barcode,6-$_.val.length,$_.val);for(var _Z=0;_Z<=5;_Z+=1){var _b=$g($_.barcode,_Z);$k[$j++]=_Z;$k[$j++]=_b;$k[$j++]=_b;for(var _c=0,_d="AEIO".length;_c<_d;_c++){var _f=$k[--$j];if(_f>=$g("AEIO",_c)){$k[$j-1]+=1}var _h=$k[$j-1];$k[$j++]=_h}$k[$j-1]=$_.barcode;var _j=$k[$j-2];var _k=$k[$j-3];var _l=$k[$j-1];$j-=3;$p(_l,_k,_j)}$P($_.text,1,$G($_.text,0,9));$p($_.text,0,65);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code39();var _s=$k[--$j];$_.args=_s;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_pzn(){var $__=$_;$_=Object.assign({},$_,bwipp_pzn.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.text1xoffset=0;$_.height=-1;$_.pzn8=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.pzn8){if($_.barcode.length!=7&&$_.barcode.length!=8){$k[$j++]="bwipp.pzn8badLength#10571";$k[$j++]="PZN8 must be 7 or 8 digits";bwipp_raiseerror()}}else{if($_.barcode.length!=6&&$_.barcode.length!=7){$k[$j++]="bwipp.pzn7badLength#10575";$k[$j++]="PZN7 must be 6 or 7 digits";bwipp_raiseerror()}}$F($_.barcode,function(){var _A=$k[--$j];if(_A<48||_A>57){$k[$j++]="bwipp.pznBadCharacter#10580";$k[$j++]="PZN must contain only digits";bwipp_raiseerror()}});var _C=$_.pzn8?9:8;$_.msglen=_C;$_.checksum=0;for(var _F=0,_E=$_.msglen-3;_F<=_E;_F+=1){$_.i=_F;var _L=$_.pzn8?1:2;$_.checksum=$f(($g($_.barcode,$_.i)-48)*($_.i+_L)+$_.checksum)}$_.checksum=$_.checksum%11;if($_.checksum==10){$k[$j++]="bwipp.pznBadInputSequence#10596";$k[$j++]="Incorrect PZN input sequence provided";bwipp_raiseerror()}if($_.barcode.length==$_.msglen-1){if($g($_.barcode,$_.msglen-2)!=$_.checksum+48){$k[$j++]="bwipp.pznBadCheckDigit#10600";$k[$j++]="Incorrect PZN check digit provided";bwipp_raiseerror()}}var _W=$s($_.msglen);$p(_W,0,45);$P(_W,1,$_.barcode);$p(_W,$_.msglen-1,$_.checksum+48);$_.msg=_W;$_.bwipp_dontdraw=true;$k[$j++]=$_.msg;$k[$j++]=$_.options;bwipp_code39();var _c=$k[--$j];$_.args=_c;$_.text=$s($_.msglen+5);$P($_.text,0,"PZN - ");$P($_.text,6,$G($_.msg,1,$_.msglen-1));$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code93(){if(!bwipp_code93.globals){var $__=$_;$_=bwipp_code93.globals={};$_.code93_encs=$a(["131112","111213","111312","111411","121113","121212","121311","111114","131211","141111","211113","211212","211311","221112","221211","231111","112113","112212","112311","122112","132111","111123","111222","111321","121122","131121","212112","212211","211122","211221","221121","222111","112122","112221","122121","123111","121131","311112","311211","321111","112131","113121","211131","121221","312111","311121","122211","111141","1111411"]);$_.code93_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.code93_sft1=-1;$_.code93_sft2=-2;$_.code93_sft3=-3;$_.code93_sft4=-4;$_.charvals=new Map;for(var _1=0;_1<=42;_1+=1){$p($_.charvals,$g($_.code93_barchars,_1),_1)}$p($_.charvals,$_.code93_sft1,43);$p($_.charvals,$_.code93_sft2,44);$p($_.charvals,$_.code93_sft3,45);$p($_.charvals,$_.code93_sft4,46);$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code93.globals);$_.dontdraw=false;$_.includecheck=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _9=$k[--$j];$_.options=_9;var _A=$k[--$j];$_.barcode=_A;if($_.barcode.length>500){$k[$j++]="bwipp.code93inputTooLong#10744";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["SFT$",$_.code93_sft1],["SFT%",$_.code93_sft2],["SFT/",$_.code93_sft3],["SFT+",$_.code93_sft4]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _I=$k[--$j];$_.msg=_I;$_.msglen=$_.msg.length;$F($_.msg,function(){var _M=$k[--$j];if(!$has($_.charvals,_M)){$k[$j++]="bwipp.code93BadCharacter#10773";$k[$j++]="Code 93 must only contain digits, capital letters, spaces, the symbols -.$/+ and shift non-data characters";bwipp_raiseerror()}});if($_.includecheck){$_.sbs=$s($_.msglen*6+25)}else{$_.sbs=$s($_.msglen*6+13)}$P($_.sbs,0,$g($_.code93_encs,47));$_.checksum1=0;$_.checksum2=0;$_.text=$s($_.msglen);for(var _Z=0,_Y=$_.msglen-1;_Z<=_Y;_Z+=1){$_.i=_Z;$_.indx=$g($_.charvals,$g($_.msg,$_.i));$P($_.sbs,$_.i*6+6,$g($_.code93_encs,$_.indx));var _n=$g($_.msg,$_.i);var _q=$_.text;var _p=$_.i;if(_n<0){_n=32}$p(_q,_p,_n);$_.checksum1=$f($_.checksum1+(($_.msglen-$_.i-1)%20+1)*$_.indx);$_.checksum2=$f($_.checksum2+(($_.msglen-$_.i)%15+1)*$_.indx)}if($_.includecheck){$_.checksum1=$_.checksum1%47;$_.checksum2=$f($_.checksum2+$_.checksum1)%47;$P($_.sbs,$_.msglen*6+6,$g($_.code93_encs,$_.checksum1));$P($_.sbs,$_.msglen*6+12,$g($_.code93_encs,$_.checksum2));$P($_.sbs,$_.msglen*6+18,$g($_.code93_encs,48))}else{$P($_.sbs,$_.msglen*6+6,$g($_.code93_encs,48))}$k[$j++]=Infinity;var _1H=$_.sbs;for(var _1I=0,_1J=_1H.length;_1I<_1J;_1I++){$k[$j++]=$g(_1H,_1I)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _1M=$_.sbs;var _1R=[];for(var _1O=0,_1P=~~(($_.sbs.length+1)/2);_1O<_1P;_1O++){_1R[_1O]=$_.height}_1R=$a(_1R);var _1V=[];for(var _1T=0,_1U=~~(($_.sbs.length+1)/2);_1T<_1U;_1T++){_1V[_1T]=0}_1V=$a(_1V);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1M;$k[$j++]="bhs";$k[$j++]=_1R;$k[$j++]="bbs";$k[$j++]=_1V;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _1d=$d();$k[$j++]=_1d;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code93ext(){if(!bwipp_code93ext.globals){var $__=$_;$_=bwipp_code93ext.globals={};$_.code93ext_extencs=$a(["^SFT%U","^SFT$A","^SFT$B","^SFT$C","^SFT$D","^SFT$E","^SFT$F","^SFT$G","^SFT$H","^SFT$I","^SFT$J","^SFT$K","^SFT$L","^SFT$M","^SFT$N","^SFT$O","^SFT$P","^SFT$Q","^SFT$R","^SFT$S","^SFT$T","^SFT$U","^SFT$V","^SFT$W","^SFT$X","^SFT$Y","^SFT$Z","^SFT%A","^SFT%B","^SFT%C","^SFT%D","^SFT%E"," ","^SFT/A","^SFT/B","^SFT/C","$","%","^SFT/F","^SFT/G","^SFT/H","^SFT/I","^SFT/J","+","^SFT/L","-",".","/","0","1","2","3","4","5","6","7","8","9","^SFT/Z","^SFT%F","^SFT%G","^SFT%H","^SFT%I","^SFT%J","^SFT%V","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","^SFT%K","^SFT%L","^SFT%M","^SFT%N","^SFT%O","^SFT%W","^SFT+A","^SFT+B","^SFT+C","^SFT+D","^SFT+E","^SFT+F","^SFT+G","^SFT+H","^SFT+I","^SFT+J","^SFT+K","^SFT+L","^SFT+M","^SFT+N","^SFT+O","^SFT+P","^SFT+Q","^SFT+R","^SFT+S","^SFT+T","^SFT+U","^SFT+V","^SFT+W","^SFT+X","^SFT+Y","^SFT+Z","^SFT%P","^SFT%Q","^SFT%R","^SFT%S","^SFT%T"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code93ext.globals);$_.dontdraw=false;$_.includetext=false;$_.parse=false;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.code93extinputTooLong#10911";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _9=$k[--$j];$_.barcode=_9;$_.barlen=$_.barcode.length;$del($_.options,"parse");$_.newcode=$s($_.barlen*6);$_.newtext=$s($_.barlen*6);$_.j=0;$_.k=0;for(var _I=0,_H=$_.barlen-1;_I<=_H;_I+=1){$_.i=_I;var _L=$g($_.barcode,$_.i);$k[$j++]=_L;if(_L>127){$k[$j-1]="bwipp.code93extbadCharacter#10939";$k[$j++]="Code 93 Extended only support ASCII characters with values 0 to 127";bwipp_raiseerror()}var _M=$k[--$j];$_.extchar=$g($_.code93ext_extencs,_M);$_.extlen=$_.extchar.length;$P($_.newcode,$_.j,$_.extchar);var _W=$g($_.barcode,$_.i);var _Z=$_.newtext;var _Y=$_.k;if(_W<32||_W==127){_W=32}$p(_Z,_Y,_W);if($_.extlen!=1){$p($_.newtext,$_.k+1,32)}$_.j=$_.j+$_.extlen;var _h=$_.extlen==1?1:2;$_.k=$_.k+_h}$_.newcode=$G($_.newcode,0,$_.j);$_.newtext=$G($_.newtext,0,$_.k);$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.newcode;$k[$j++]=$_.options;bwipp_code93();var _r=$k[--$j];$_.args=_r;if($_.includetext){$_.txt=$g($_.args,"txt");$p($g($_.txt,0),0,$_.newtext);$p($_.args,"txt",$_.txt)}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_interleaved2of5(){if(!bwipp_interleaved2of5.globals){var $__=$_;$_=bwipp_interleaved2of5.globals={};$_.interleaved2of5_encs=$a(["11221","21112","12112","22111","11212","21211","12211","11122","21121","12121","1111","2111"]);$_.interleaved2of5_barchars="0123456789";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_interleaved2of5.globals);$_.dontdraw=false;$_.includecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.interleaved2of5inputTooLong#11046";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.interleaved2of5badCharacter#11059";$k[$j++]="Interleaved 2 of 5 must contain only digits";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;if($_.barlen%2==0&&$_.includecheck||$_.barlen%2!=0&&!$_.includecheck){$_.pad=$s($_.barlen+1);$p($_.pad,0,48);$P($_.pad,1,$_.barcode);$_.barcode=$_.pad;$_.barlen=$_.barlen+1}if($_.includecheck){$_.checksum=0;for(var _N=0,_M=$_.barlen-1;_N<=_M;_N+=1){$_.i=_N;if($_.i%2==0){$_.checksum=$_.checksum+($g($_.barcode,$_.i)-48)*3}else{$_.checksum=$_.checksum+($g($_.barcode,$_.i)-48)}}$_.checksum=(10-$_.checksum%10)%10;$_.pad=$s($_.barlen+1);$P($_.pad,0,$_.barcode);$p($_.pad,$_.barlen,$_.checksum+48);$_.barcode=$_.pad;$_.barlen=$_.barlen+1}$_.sbs=$s($_.barlen*5+8);$P($_.sbs,0,$g($_.interleaved2of5_encs,10));for(var _n=0,_m=$_.barlen-1;_n<=_m;_n+=2){$_.i=_n;$X($_.interleaved2of5_barchars,$G($_.barcode,$_.i,1));$j--;var _r=$k[--$j];$_.indx=_r.length;$j-=2;$_.enca=$g($_.interleaved2of5_encs,$_.indx);$X($_.interleaved2of5_barchars,$G($_.barcode,$_.i+1,1));$j--;var _x=$k[--$j];$_.indx=_x.length;$j-=2;$_.encb=$g($_.interleaved2of5_encs,$_.indx);$_.intl=$s($_.enca.length*2);for(var _14=0,_13=$_.enca.length-1;_14<=_13;_14+=1){$_.j=_14;$_.achar=$g($_.enca,$_.j);$_.bchar=$g($_.encb,$_.j);$p($_.intl,$_.j*2,$_.achar);$p($_.intl,$_.j*2+1,$_.bchar)}$P($_.sbs,$_.i*5+4,$_.intl)}$P($_.sbs,$_.barlen*5+4,$g($_.interleaved2of5_encs,11));var _1U=$_.barcode;var _1T=0;var _1S=$_.includecheck&&!$_.includecheckintext?$_.barlen-1:$_.barlen;$_.text=$G(_1U,_1T,_1S);$k[$j++]=Infinity;var _1X=$_.sbs;for(var _1Y=0,_1Z=_1X.length;_1Y<_1Z;_1Y++){$k[$j++]=$g(_1X,_1Y)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _1c=$_.sbs;var _1h=[];for(var _1e=0,_1f=~~(($_.sbs.length+1)/2);_1e<_1f;_1e++){_1h[_1e]=$_.height}_1h=$a(_1h);var _1l=[];for(var _1j=0,_1k=~~(($_.sbs.length+1)/2);_1j<_1k;_1j++){_1l[_1j]=0}_1l=$a(_1l);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1c;$k[$j++]="bhs";$k[$j++]=_1h;$k[$j++]="bbs";$k[$j++]=_1l;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="barratio";$k[$j++]=2;$k[$j++]="spaceratio";$k[$j++]=2;$k[$j++]="opt";$k[$j++]=$_.options;var _1t=$d();$k[$j++]=_1t;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_itf14(){var $__=$_;$_=Object.assign({},$_,bwipp_itf14.globals);$_.dontdraw=false;$_.includetext=false;$_.includecheckintext=true;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.text1yoffset=-3.5;$_.height=-1;$_.width=0;$_.modunit=1;$_.showborder=true;$_.borderwidth=4;$_.borderleft=10;$_.borderright=10;$_.bordertop=-.55;$_.borderbottom=-.55;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.text=$_.barcode;var _5=$_.barcode;$k[$j++]=$s(_5.length);$k[$j++]=0;$F(_5,function(){var _7=$k[$j-1];if(_7!=32){var _8=$k[--$j];var _9=$k[--$j];var _A=$k[$j-1];$p(_A,_9,_8);$k[$j++]=_9+1}else{$j--}});var _B=$k[--$j];var _C=$k[--$j];$_.barcode=$G(_C,0,_B);$_.hasspace=$_.text.length!=$_.barcode.length;if($_.barcode.length!=13&&$_.barcode.length!=14){$k[$j++]="bwipp.itf14badLength#11265";$k[$j++]="ITF-14 must be 13 or 14 digits";bwipp_raiseerror()}var _I=$_.barcode;for(var _J=0,_K=_I.length;_J<_K;_J++){var _L=$g(_I,_J);if(_L<48||_L>57){$k[$j++]="bwipp.itf14badCharacter#11269";$k[$j++]="ITF-14 must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _M=0;_M<=12;_M+=1){$_.i=_M;var _U=$_.checksum;var _T=$g($_.barcode,$_.i)-48;if($_.i%2==0){_T=_T*3}$_.checksum=$f(_U+_T)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==14){if($g($_.barcode,13)!=$_.checksum+48){$k[$j++]="bwipp.itf14badCheckDigit#11284";$k[$j++]="Incorrect ITF-14 check digit provided";bwipp_raiseerror()}}else{var _b=$s(14);$P(_b,0,$_.barcode);$p(_b,13,$_.checksum+48);$_.barcode=_b;var _g=$_.hasspace?2:1;var _h=$s($_.text.length+_g);$P(_h,_h.length-2," ");$p(_h,_h.length-1,$_.checksum+48);$P(_h,0,$_.text);$_.text=_h}if($_.height==-1){if(!($_.propspec&&$_.hnom!=-1)){$_.height=.5}}$p($_.options,"showborder",$_.showborder);$p($_.options,"borderwidth",$_.borderwidth);$p($_.options,"borderleft",$_.borderleft);$p($_.options,"borderright",$_.borderright);$p($_.options,"bordertop",$_.bordertop);$p($_.options,"borderbottom",$_.borderbottom);$p($_.options,"height",$_.height);$p($_.options,"text1yoffset",$_.text1yoffset);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_interleaved2of5();var _15=$k[--$j];$_.args=_15;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_identcode(){var $__=$_;$_=Object.assign({},$_,bwipp_identcode.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=11&&$_.barcode.length!=12){$k[$j++]="bwipp.identcodeBadLength#11395";$k[$j++]="Deutsche Post Identcode must be 11 or 12 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.identcodeBadCharacter#11399";$k[$j++]="Deutsche Post Identcode must contain only digits";bwipp_raiseerror()}});$_.checksum=0;for(var _8=0;_8<=10;_8+=1){$_.i=_8;var _H=$_.checksum;var _G=$g($_.barcode,$_.i)-48;if($_.i%2==0){_G=_G*4}else{_G=_G*9}$_.checksum=$f(_H+_G)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==12){if($g($_.barcode,11)!=$_.checksum+48){$k[$j++]="bwipp.identcodeBadCheckDigit#11414";$k[$j++]="Incorrect Deutsche Post Identcode check digit provided";bwipp_raiseerror()}}var _O=$s(12);$P(_O,0,$_.barcode);$p(_O,11,$_.checksum+48);$_.barcode=_O;$_.text=$Z($s(16)," . . ");$P($_.text,0,$G($_.barcode,0,2));$P($_.text,3,$G($_.barcode,2,3));$P($_.text,7,$G($_.barcode,5,3));$P($_.text,11,$G($_.barcode,8,3));$P($_.text,15,$G($_.barcode,11,1));$p($_.options,"includecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_interleaved2of5();var _l=$k[--$j];$_.args=_l;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_leitcode(){var $__=$_;$_=Object.assign({},$_,bwipp_leitcode.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=13&&$_.barcode.length!=14){$k[$j++]="bwipp.leitcodeBadLength#11504";$k[$j++]="Deutsche Post Leitcode must be 13 or 14 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.leitcodeBadCharacter#11508";$k[$j++]="Deutsche Post Leitcode must contain only digits";bwipp_raiseerror()}});$_.checksum=0;for(var _8=0;_8<=12;_8+=1){$_.i=_8;var _H=$_.checksum;var _G=$g($_.barcode,$_.i)-48;if($_.i%2==0){_G=_G*4}else{_G=_G*9}$_.checksum=$f(_H+_G)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==14){if($g($_.barcode,13)!=$_.checksum+48){$k[$j++]="bwipp.leitcodeBadCheckDigit#11523";$k[$j++]="Incorrect Deutsche Post Leitcode check digit provided";bwipp_raiseerror()}}var _O=$s(14);$P(_O,0,$_.barcode);$p(_O,13,$_.checksum+48);$_.barcode=_O;$_.text=$Z($s(18)," . . . ");$P($_.text,0,$G($_.barcode,0,5));$P($_.text,6,$G($_.barcode,5,3));$P($_.text,10,$G($_.barcode,8,3));$P($_.text,14,$G($_.barcode,11,2));$P($_.text,17,$G($_.barcode,13,1));$p($_.options,"includecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_interleaved2of5();var _l=$k[--$j];$_.args=_l;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_databaromni(){if(!bwipp_databaromni.globals){var $__=$_;$_=bwipp_databaromni.globals={};$_.databaromni_tab164=$a([160,0,12,4,8,1,161,1,960,161,10,6,6,3,80,10,2014,961,8,8,4,5,31,34,2714,2015,6,10,3,6,10,70,2840,2715,4,12,1,8,1,126]);$_.databaromni_tab154=$a([335,0,5,10,2,7,4,84,1035,336,7,8,4,5,20,35,1515,1036,9,6,6,3,48,10,1596,1516,11,4,8,1,81,1]);$_.databaromni_checkweights=$a([1,3,9,27,2,6,18,54,58,72,24,8,29,36,12,4,74,51,17,32,37,65,48,16,64,34,23,69,49,68,46,59]);$_.databaromni_checkwidths=$a([3,8,2,1,1,3,5,5,1,1,3,3,7,1,1,3,1,9,1,1,2,7,4,1,1,2,5,6,1,1,2,3,8,1,1,1,5,7,1,1,1,3,9,1,1]);$_.databaromni_seppad=$a([0,0,0,0]);$_.databaromni_findersep=$a([0,0,0,0,0,0,0,0,0,0,1,0,0]);$_.databaromni_f3pat=$a([1,1,1,1,1,1,1,1,1,0,1,1,1]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databaromni.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.linkage=false;$_.format="omni";$_.barxmult=33;$_.modunit=1;bwipp_processoptions();var _7=$k[--$j];$_.options=_7;var _8=$k[--$j];$_.barcode=_8;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.databaromniBadLength#11656";$k[$j++]="GS1 DataBar Omnidirectional must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.databaromniBadAI#11659";$k[$j++]="GS1 DataBar Omnidirectional must begin with (01) application identifier";bwipp_raiseerror()}var _H=$G($_.barcode,4,$_.barcode.length-4);for(var _I=0,_J=_H.length;_I<_J;_I++){var _K=$g(_H,_I);if(_K<48||_K>57){$k[$j++]="bwipp.databaromniBadCharacter#11663";$k[$j++]="GS1 DataBar Omnidirectional must contain only digits";bwipp_raiseerror()}}if($_.barxmult<33){$k[$j++]="bwipp.databarStackedOmniBarXmult#11668";$k[$j++]="GS1 DataBar Stacked Omnidirectional must have a barxmult of at least 33";bwipp_raiseerror()}if($ne($_.format,"omni")&&$ne($_.format,"stacked")&&$ne($_.format,"stackedomni")&&$ne($_.format,"truncated")){$k[$j++]="bwipp.databaromniBadFormat#11672";$k[$j++]="Valid formats are omni, stacked, stackedomni and truncated";bwipp_raiseerror()}if($eq($_.format,"truncated")&&$_.height==-1){$_.height=13/72}$_.checksum=0;for(var _S=0;_S<=12;_S+=1){$_.i=_S;var _a=$_.checksum;var _Z=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_Z=_Z*3}$_.checksum=$f(_a+_Z)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.databaromniBadCheckDigit#11694";$k[$j++]="Incorrect GS1 DataBar Omnidirectional check digit provided";bwipp_raiseerror()}}var _h=$s(18);$P(_h,0,$_.barcode);$p(_h,17,$_.checksum+48);$_.barcode=_h;$_.txt=$a($_.barcode.length);for(var _o=0,_n=$_.barcode.length-1;_o<=_n;_o+=1){$_.i=_o;$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$_.ncr=function(){var _v=$k[--$j];var _w=$k[--$j];var _x=$f(_w-_v);if(_v<$f(_w-_v)){var _=_x;_x=_v;_v=_}$k[$j++]=_x;$k[$j++]=1;$k[$j++]=1;for(var _z=_w,_y=_v+1;_z>=_y;_z-=1){var _10=$k[--$j];var _11=$k[$j-1];var _12=$k[$j-2];$k[$j++]=_10*_z;if($le(_11,_12)){var _13=$k[--$j];var _14=$k[--$j];$k[$j++]=_14+1;$k[$j++]=~~(_13/_14)}}for(;;){var _16=$k[$j-2];var _17=$k[$j-3];if($gt(_16,_17)){break}var _18=$k[--$j];var _19=$k[--$j];$k[$j++]=_19+1;$k[$j++]=~~(_18/_19)}var _1A=$k[--$j];$j--;$k[$j-1]=_1A};$_.getRSSwidths=function(){var _1E=$k[--$j];$_.oe=_1E;var _1F=$k[--$j];$_.el=_1F;var _1G=$k[--$j];$_.mw=_1G;var _1H=$k[--$j];$_.nm=_1H;var _1I=$k[--$j];$_.val=_1I;$_.out=$a($_.el);$_.mask=0;for(var _1N=0,_1M=$_.el-2;_1N<=_1M;_1N+=1){$_.bar=_1N;$_.ew=1;$_.mask=$_.mask|$bs(1,$_.bar);for(;;){$k[$j++]="sval";$k[$j++]=$f($_.nm-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _1U=$k[--$j];var _1V=$k[--$j];$_[_1V]=_1U;if($_.oe&&$_.mask==0&&$f($_.nm-$_.ew-$_.el*2+$_.bar*2)>=-2){$k[$j++]="sval";$k[$j++]=$_.sval;$k[$j++]=$f($_.nm-$_.ew-$_.el+$_.bar);$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _1j=$k[--$j];var _1k=$k[--$j];var _1l=$k[--$j];$_[_1l]=$f(_1k-_1j)}if($f($_.el-$_.bar)>2){$_.lval=0;for(var _1u=$f($_.nm-$_.ew-$_.el+$_.bar)+2,_1t=$_.mw+1;_1u>=_1t;_1u-=1){$k[$j++]=$f($_.nm-_1u-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-3;$_.ncr();var _1z=$k[--$j];$_.lval=$f(_1z+$_.lval)}$_.sval=$f($_.sval-$_.lval*($_.el-$_.bar-1))}else{if($f($_.nm-$_.ew)>$_.mw){$_.sval=$_.sval-1}}$_.val=$f($_.val-$_.sval);if($_.val<0){break}$_.ew=$_.ew+1;$_.mask=$_.mask&~$bs(1,$_.bar)}$_.val=$f($_.val+$_.sval);$_.nm=$f($_.nm-$_.ew);$p($_.out,$_.bar,$_.ew)}$p($_.out,$_.el-1,$_.nm);$k[$j++]=$_.out};$k[$j++]=Infinity;var _2R=$_.linkage?1:0;var _2T=$G($_.barcode,4,13);$k[$j++]=_2R;for(var _2U=0,_2V=_2T.length;_2U<_2V;_2U++){$k[$j++]=$g(_2T,_2U)-48}$_.binval=$a();for(var _2X=0;_2X<=12;_2X+=1){$_.i=_2X;var _2Y=$_.binval;var _2Z=$_.i;$p(_2Y,_2Z+1,$f($g(_2Y,_2Z+1)+$g($_.binval,$_.i)%4537077*10));$p($_.binval,$_.i,~~($g($_.binval,$_.i)/4537077))}$_.right=$g($_.binval,13)%4537077;var _2l=$_.binval;$p(_2l,13,~~($g(_2l,13)/4537077));$_.left=0;$_.i=true;for(var _2n=0;_2n<=13;_2n+=1){$_.j=_2n;var _2q=$g($_.binval,$_.j);$k[$j++]=_2q;if(_2q==0&&$_.i){$j--}else{$_.i=false;$k[$j++]="left";$k[$j++]=$_.left;var _2u=$k[$j-3];var _2v=$k[$j-1];var _2w=$k[$j-2];$j-=3;$_[_2w]=$f(_2v+_2u*$pow(10,13-$_.j))}}$_.d1=~~($_.left/1597);$_.d2=$_.left%1597;$_.d3=~~($_.right/1597);$_.d4=$_.right%1597;$_.i=0;for(;;){if($_.d1<=$g($_.databaromni_tab164,$_.i)){$q($G($_.databaromni_tab164,$_.i+1,7));var _36=$k[--$j];$_.d1te=_36;var _37=$k[--$j];$_.d1to=_37;var _38=$k[--$j];$_.d1mwe=_38;var _39=$k[--$j];$_.d1mwo=_39;var _3A=$k[--$j];$_.d1ele=_3A;var _3B=$k[--$j];$_.d1elo=_3B;var _3C=$k[--$j];$_.d1gs=_3C;break}$_.i=$_.i+8}$_.i=0;for(;;){if($_.d2<=$g($_.databaromni_tab154,$_.i)){$q($G($_.databaromni_tab154,$_.i+1,7));var _3J=$k[--$j];$_.d2te=_3J;var _3K=$k[--$j];$_.d2to=_3K;var _3L=$k[--$j];$_.d2mwe=_3L;var _3M=$k[--$j];$_.d2mwo=_3M;var _3N=$k[--$j];$_.d2ele=_3N;var _3O=$k[--$j];$_.d2elo=_3O;var _3P=$k[--$j];$_.d2gs=_3P;break}$_.i=$_.i+8}$_.i=0;for(;;){if($_.d3<=$g($_.databaromni_tab164,$_.i)){$q($G($_.databaromni_tab164,$_.i+1,7));var _3W=$k[--$j];$_.d3te=_3W;var _3X=$k[--$j];$_.d3to=_3X;var _3Y=$k[--$j];$_.d3mwe=_3Y;var _3Z=$k[--$j];$_.d3mwo=_3Z;var _3a=$k[--$j];$_.d3ele=_3a;var _3b=$k[--$j];$_.d3elo=_3b;var _3c=$k[--$j];$_.d3gs=_3c;break}$_.i=$_.i+8}$_.i=0;for(;;){if($_.d4<=$g($_.databaromni_tab154,$_.i)){$q($G($_.databaromni_tab154,$_.i+1,7));var _3j=$k[--$j];$_.d4te=_3j;var _3k=$k[--$j];$_.d4to=_3k;var _3l=$k[--$j];$_.d4mwe=_3l;var _3m=$k[--$j];$_.d4mwo=_3m;var _3n=$k[--$j];$_.d4ele=_3n;var _3o=$k[--$j];$_.d4elo=_3o;var _3p=$k[--$j];$_.d4gs=_3p;break}$_.i=$_.i+8}$k[$j++]="d1wo";$k[$j++]=~~($f($_.d1-$_.d1gs)/$_.d1te);$k[$j++]=$_.d1elo;$k[$j++]=$_.d1mwo;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _3w=$k[--$j];var _3x=$k[--$j];$_[_3x]=_3w;$k[$j++]="d1we";$k[$j++]=$f($_.d1-$_.d1gs)%$_.d1te;$k[$j++]=$_.d1ele;$k[$j++]=$_.d1mwe;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _43=$k[--$j];var _44=$k[--$j];$_[_44]=_43;$k[$j++]="d2wo";$k[$j++]=$f($_.d2-$_.d2gs)%$_.d2to;$k[$j++]=$_.d2elo;$k[$j++]=$_.d2mwo;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _4A=$k[--$j];var _4B=$k[--$j];$_[_4B]=_4A;$k[$j++]="d2we";$k[$j++]=~~($f($_.d2-$_.d2gs)/$_.d2to);$k[$j++]=$_.d2ele;$k[$j++]=$_.d2mwe;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _4H=$k[--$j];var _4I=$k[--$j];$_[_4I]=_4H;$k[$j++]="d3wo";$k[$j++]=~~($f($_.d3-$_.d3gs)/$_.d3te);$k[$j++]=$_.d3elo;$k[$j++]=$_.d3mwo;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _4O=$k[--$j];var _4P=$k[--$j];$_[_4P]=_4O;$k[$j++]="d3we";$k[$j++]=$f($_.d3-$_.d3gs)%$_.d3te;$k[$j++]=$_.d3ele;$k[$j++]=$_.d3mwe;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _4V=$k[--$j];var _4W=$k[--$j];$_[_4W]=_4V;$k[$j++]="d4wo";$k[$j++]=$f($_.d4-$_.d4gs)%$_.d4to;$k[$j++]=$_.d4elo;$k[$j++]=$_.d4mwo;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _4c=$k[--$j];var _4d=$k[--$j];$_[_4d]=_4c;$k[$j++]="d4we";$k[$j++]=~~($f($_.d4-$_.d4gs)/$_.d4to);$k[$j++]=$_.d4ele;$k[$j++]=$_.d4mwe;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _4j=$k[--$j];var _4k=$k[--$j];$_[_4k]=_4j;$_.d1w=$a(8);for(var _4m=0;_4m<=3;_4m+=1){$_.i=_4m;$p($_.d1w,$_.i*2,$g($_.d1wo,$_.i));$p($_.d1w,$_.i*2+1,$g($_.d1we,$_.i))}$_.d2w=$a(8);for(var _4y=0;_4y<=3;_4y+=1){$_.i=_4y;$p($_.d2w,7-$_.i*2,$g($_.d2wo,$_.i));$p($_.d2w,6-$_.i*2,$g($_.d2we,$_.i))}$_.d3w=$a(8);for(var _5A=0;_5A<=3;_5A+=1){$_.i=_5A;$p($_.d3w,7-$_.i*2,$g($_.d3wo,$_.i));$p($_.d3w,6-$_.i*2,$g($_.d3we,$_.i))}$_.d4w=$a(8);for(var _5M=0;_5M<=3;_5M+=1){$_.i=_5M;$p($_.d4w,$_.i*2,$g($_.d4wo,$_.i));$p($_.d4w,$_.i*2+1,$g($_.d4we,$_.i))}$k[$j++]=Infinity;$q($_.d1w);$q($_.d2w);$q($_.d3w);$q($_.d4w);$_.widths=$a();$_.checksum=0;for(var _5b=0;_5b<=31;_5b+=1){$_.i=_5b;$_.checksum=$f($_.checksum+$g($_.widths,$_.i)*$g($_.databaromni_checkweights,$_.i))}$_.checksum=$_.checksum%79;if($_.checksum>=8){$_.checksum=$_.checksum+1}if($_.checksum>=72){$_.checksum=$_.checksum+1}$_.checklt=$G($_.databaromni_checkwidths,~~($_.checksum/9)*5,5);$_.checkrtrev=$G($_.databaromni_checkwidths,$_.checksum%9*5,5);$_.checkrt=$a(5);for(var _5s=0;_5s<=4;_5s+=1){$_.i=_5s;$p($_.checkrt,$_.i,$g($_.checkrtrev,4-$_.i))}if($eq($_.format,"omni")||$eq($_.format,"truncated")){$k[$j++]=Infinity;$k[$j++]=1;$q($_.d1w);$q($_.checklt);$q($_.d2w);$q($_.d4w);$q($_.checkrt);$q($_.d3w);$k[$j++]=1;$k[$j++]=1;$_.sbs=$a();if($_.height==-1){$_.height=33/72}$k[$j++]=Infinity;var _67=$_.sbs;var _6C=[];for(var _69=0,_6A=~~(($_.sbs.length+1)/2);_69<_6A;_69++){_6C[_69]=$_.height}_6C=$a(_6C);var _6G=[];for(var _6E=0,_6F=~~(($_.sbs.length+1)/2);_6E<_6F;_6E++){_6G[_6E]=0}_6G=$a(_6G);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_67;$k[$j++]="bhs";$k[$j++]=_6C;$k[$j++]="bbs";$k[$j++]=_6G;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="borderleft";$k[$j++]=0;$k[$j++]="borderright";$k[$j++]=0;$k[$j++]="opt";$k[$j++]=$_.options;var _6J=$d();$k[$j++]=_6J;if($_._render){bwipp_renlinear()}}else{$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=1;$q($_.d1w);$q($_.checklt);$q($_.d2w);$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$_.top=$a();$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=1;$q($_.d4w);$q($_.checkrt);$q($_.d3w);$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$_.bot=$a();for(var _6R=0;_6R<=24;_6R+=2){$_.i=_6R;for(var _6V=0,_6W=$g($_.top,$_.i);_6V<_6W;_6V++){$k[$j++]=0}for(var _6a=0,_6b=$g($_.top,$_.i+1);_6a<_6b;_6a++){$k[$j++]=1}}$as($a(50));var _6d=$k[--$j];$_.top=_6d;for(var _6e=0;_6e<=24;_6e+=2){$_.i=_6e;for(var _6i=0,_6j=$g($_.bot,$_.i);_6i<_6j;_6i++){$k[$j++]=1}for(var _6n=0,_6o=$g($_.bot,$_.i+1);_6n<_6o;_6n++){$k[$j++]=0}}$as($a(50));var _6q=$k[--$j];$_.bot=_6q;if($eq($_.format,"stacked")){$_.sep=$a(50);$p($_.sep,0,0);for(var _6u=1;_6u<=49;_6u+=1){$_.i=_6u;if($eq($g($_.top,$_.i),$g($_.bot,$_.i))){$p($_.sep,$_.i,1-$g($_.top,$_.i))}else{$p($_.sep,$_.i,1-$g($_.sep,$_.i-1))}}$P($_.sep,0,$_.databaromni_seppad);$P($_.sep,46,$_.databaromni_seppad);$k[$j++]=Infinity;$q($_.top);$q($_.sep);$q($_.bot);$_.pixs=$a();$_.rowmult=$a([5,1,7]);$_.pixy=13}if($eq($_.format,"stackedomni")){$k[$j++]=Infinity;$F($_.top,function(){var _7I=$k[--$j];$k[$j++]=1-_7I});$_.sep1=$a();$P($_.sep1,0,$_.databaromni_seppad);$P($_.sep1,46,$_.databaromni_seppad);for(var _7L=18;_7L<=30;_7L+=1){$_.i=_7L;if($g($_.top,$_.i)==0){if($g($_.top,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep1,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _7X=$k[--$j];$p($_.sep1,$_.i,_7X)}$k[$j++]=Infinity;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;for(var _7Z=0;_7Z<21;_7Z++){$k[$j++]=0;$k[$j++]=1}$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$_.sep2=$a();$k[$j++]=Infinity;$F($_.bot,function(){var _7b=$k[--$j];$k[$j++]=1-_7b});$_.sep3=$a();$P($_.sep3,0,$_.databaromni_seppad);$P($_.sep3,46,$_.databaromni_seppad);for(var _7e=19;_7e<=31;_7e+=1){$_.i=_7e;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep3,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _7q=$k[--$j];$p($_.sep3,$_.i,_7q)}var _7x=true;for(var _7s=0;_7s<=12;_7s+=1){_7x=_7x&&$eq($g($_.bot,_7s+19),$g($_.databaromni_f3pat,_7s))}if(_7x){$P($_.sep3,19,$_.databaromni_findersep)}$k[$j++]=Infinity;$q($_.top);$q($_.sep1);$q($_.sep2);$q($_.sep3);$q($_.bot);$_.pixs=$a();$_.rowmult=$a([$_.barxmult,1,1,1,$_.barxmult]);$_.pixy=$_.barxmult*2+3}$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",50],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",50/72],["borderleft",0],["borderright",0],["bordertop",0],["borderbottom",0],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}}$_=$__}function bwipp_databarstacked(){var $__=$_;$_=Object.assign({},$_,bwipp_databarstacked.globals);$_.dontdraw=false;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.databarstackedBadLength#12140";$k[$j++]="GS1 DataBar Stacked must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.databarstackedBadAI#12143";$k[$j++]="GS1 DataBar Stacked must begin with (01) application identifier";bwipp_raiseerror()}var _A=$G($_.barcode,4,$_.barcode.length-4);for(var _B=0,_C=_A.length;_B<_C;_B++){var _D=$g(_A,_B);if(_D<48||_D>57){$k[$j++]="bwipp.databarstackedBadCharacter#12147";$k[$j++]="GS1 DataBar Stacked must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _E=0;_E<=12;_E+=1){$_.i=_E;var _M=$_.checksum;var _L=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_L=_L*3}$_.checksum=$f(_M+_L)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.databarstackedBadCheckDigit#12162";$k[$j++]="Incorrect GS1 DataBar Stacked check digit provided";bwipp_raiseerror()}}var _T=$s(18);$P(_T,0,$_.barcode);$p(_T,17,$_.checksum+48);$_.barcode=_T;$p($_.options,"format","stacked");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_databaromni();var _Z=$k[--$j];$_.args=_Z;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarstackedomni(){var $__=$_;$_=Object.assign({},$_,bwipp_databarstackedomni.globals);$_.dontdraw=false;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.databarstackedomniBadLength#12266";$k[$j++]="GS1 DataBar Stacked Omnidirectional must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.databarstackedomniBadAI#12269";$k[$j++]="GS1 DataBar Stacked Omnidirectional must begin with (01) application identifier";bwipp_raiseerror()}var _A=$G($_.barcode,4,$_.barcode.length-4);for(var _B=0,_C=_A.length;_B<_C;_B++){var _D=$g(_A,_B);if(_D<48||_D>57){$k[$j++]="bwipp.databarstackedomniBadCharacter#12273";$k[$j++]="GS1 DataBar Stacked Omnidirectional must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _E=0;_E<=12;_E+=1){$_.i=_E;var _M=$_.checksum;var _L=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_L=_L*3}$_.checksum=$f(_M+_L)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.databarstackedomniBadCheckDigit#12288";$k[$j++]="Incorrect GS1 DataBar Stacked Omnidirectional check digit provided";bwipp_raiseerror()}}var _T=$s(18);$P(_T,0,$_.barcode);$p(_T,17,$_.checksum+48);$_.barcode=_T;$p($_.options,"format","stackedomni");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_databaromni();var _Z=$k[--$j];$_.args=_Z;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databartruncated(){var $__=$_;$_=Object.assign({},$_,bwipp_databartruncated.globals);$_.dontdraw=false;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.databartruncatedBadLength#12392";$k[$j++]="GS1 DataBar Truncated must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.databartruncatedBadAI#12395";$k[$j++]="GS1 DataBar Truncated must begin with (01) application identifier";bwipp_raiseerror()}var _A=$G($_.barcode,4,$_.barcode.length-4);for(var _B=0,_C=_A.length;_B<_C;_B++){var _D=$g(_A,_B);if(_D<48||_D>57){$k[$j++]="bwipp.databartruncatedBadCharacter#12399";$k[$j++]="GS1 DataBar Truncated must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _E=0;_E<=12;_E+=1){$_.i=_E;var _M=$_.checksum;var _L=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_L=_L*3}$_.checksum=$f(_M+_L)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.databartruncatedBadCheckDigit#12414";$k[$j++]="Incorrect GS1 DataBar Truncated check digit provided";bwipp_raiseerror()}}var _T=$s(18);$P(_T,0,$_.barcode);$p(_T,17,$_.checksum+48);$_.barcode=_T;$p($_.options,"format","truncated");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_databaromni();var _Z=$k[--$j];$_.args=_Z;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_databarlimited(){if(!bwipp_databarlimited.globals){var $__=$_;$_=bwipp_databarlimited.globals={};$_.databarlimited_linkval=$a([2,0,1,5,1,3,3,5,3,1,0,9,6]);$_.databarlimited_tab267=$a([183063,0,17,9,6,3,6538,28,820063,183064,13,13,5,4,875,728,1000775,820064,9,17,3,6,28,6454,1491020,1000776,15,11,5,4,2415,203,1979844,1491021,11,15,4,5,203,2408,1996938,1979845,19,7,8,1,17094,1,2013570,1996939,7,19,1,8,1,16632]);$_.databarlimited_checkweights=$a([1,3,9,27,81,65,17,51,64,14,42,37,22,66,20,60,2,6,18,54,73,41,34,13,39,28,84,74]);$k[$j++]=Infinity;for(var _3=0;_3<=43;_3+=1){$k[$j++]=_3}$k[$j++]=45;$k[$j++]=52;$k[$j++]=57;for(var _4=63;_4<=66;_4+=1){$k[$j++]=_4}for(var _5=73;_5<=79;_5+=1){$k[$j++]=_5}$k[$j++]=82;for(var _6=126;_6<=130;_6+=1){$k[$j++]=_6}$k[$j++]=132;for(var _7=141;_7<=146;_7+=1){$k[$j++]=_7}for(var _8=210;_8<=217;_8+=1){$k[$j++]=_8}$k[$j++]=220;for(var _9=316;_9<=320;_9+=1){$k[$j++]=_9}$k[$j++]=322;$k[$j++]=323;$k[$j++]=326;$k[$j++]=337;var _A=$a();$_.checkseq=_A;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarlimited.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.linkage=false;$_.modunit=1;bwipp_processoptions();var _B=$k[--$j];$_.options=_B;var _C=$k[--$j];$_.barcode=_C;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length!=17&&$_.barcode.length!=18){$k[$j++]="bwipp.databarlimitedBadLength#12576";$k[$j++]="GS1 DataBar Limited must be 13 or 14 digits";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"(01)")){$k[$j++]="bwipp.databarlimitedBadAI#12579";$k[$j++]="GS1 DataBar Limited must begin with (01) application identifier";bwipp_raiseerror()}var _K=$g($_.barcode,4);if(_K<48||_K>49){$k[$j++]="bwipp.databarlimitedBadStartDigit#12582";$k[$j++]="GS1 DataBar Limited must begin with 0 or 1";bwipp_raiseerror()}var _N=$G($_.barcode,5,$_.barcode.length-5);for(var _O=0,_P=_N.length;_O<_P;_O++){var _Q=$g(_N,_O);if(_Q<48||_Q>57){$k[$j++]="bwipp.databarlimitedBadCharacter#12586";$k[$j++]="GS1 DataBar Limited must contain only digits";bwipp_raiseerror()}}$_.checksum=0;for(var _R=0;_R<=12;_R+=1){$_.i=_R;var _Z=$_.checksum;var _Y=$g($_.barcode,$_.i+4)-48;if($_.i%2==0){_Y=_Y*3}$_.checksum=$f(_Z+_Y)}$_.checksum=(10-$_.checksum%10)%10;if($_.barcode.length==18){if($g($_.barcode,17)!=$_.checksum+48){$k[$j++]="bwipp.databarlimitedBadCheckDigit#12607";$k[$j++]="Incorrect GS1 DataBar Limited check digit provided";bwipp_raiseerror()}}var _g=$s(18);$P(_g,0,$_.barcode);$p(_g,17,$_.checksum+48);$_.barcode=_g;$k[$j++]=Infinity;var _k=$G($_.barcode,4,13);for(var _l=0,_m=_k.length;_l<_m;_l++){$k[$j++]=$g(_k,_l)-48}$_.binval=$a();if($_.linkage){for(var _r=0,_q=$_.binval.length-1;_r<=_q;_r+=1){$_.i=_r;$p($_.binval,$_.i,$f($g($_.binval,$_.i)+$g($_.databarlimited_linkval,$_.i)))}}$_.txt=$a($_.barcode.length);for(var _13=0,_12=$_.barcode.length-1;_13<=_12;_13+=1){$_.i=_13;$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$_.ncr=function(){var _1A=$k[--$j];var _1B=$k[--$j];var _1C=$f(_1B-_1A);if(_1A<$f(_1B-_1A)){var _=_1C;_1C=_1A;_1A=_}$k[$j++]=_1C;$k[$j++]=1;$k[$j++]=1;for(var _1E=_1B,_1D=_1A+1;_1E>=_1D;_1E-=1){var _1F=$k[--$j];var _1G=$k[$j-1];var _1H=$k[$j-2];$k[$j++]=_1F*_1E;if($le(_1G,_1H)){var _1I=$k[--$j];var _1J=$k[--$j];$k[$j++]=_1J+1;$k[$j++]=~~(_1I/_1J)}}for(;;){var _1L=$k[$j-2];var _1M=$k[$j-3];if($gt(_1L,_1M)){break}var _1N=$k[--$j];var _1O=$k[--$j];$k[$j++]=_1O+1;$k[$j++]=~~(_1N/_1O)}var _1P=$k[--$j];$j--;$k[$j-1]=_1P};$_.getRSSwidths=function(){var _1T=$k[--$j];$_.oe=_1T;var _1U=$k[--$j];$_.el=_1U;var _1V=$k[--$j];$_.mw=_1V;var _1W=$k[--$j];$_.nm=_1W;var _1X=$k[--$j];$_.val=_1X;$_.out=$a($_.el);$_.mask=0;for(var _1c=0,_1b=$_.el-2;_1c<=_1b;_1c+=1){$_.bar=_1c;$_.ew=1;$_.mask=$_.mask|$bs(1,$_.bar);for(;;){$k[$j++]="sval";$k[$j++]=$f($_.nm-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _1j=$k[--$j];var _1k=$k[--$j];$_[_1k]=_1j;if($_.oe&&$_.mask==0&&$f($_.nm-$_.ew-$_.el*2+$_.bar*2)>=-2){$k[$j++]="sval";$k[$j++]=$_.sval;$k[$j++]=$f($_.nm-$_.ew-$_.el+$_.bar);$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _1y=$k[--$j];var _1z=$k[--$j];var _20=$k[--$j];$_[_20]=$f(_1z-_1y)}if($f($_.el-$_.bar)>2){$_.lval=0;for(var _29=$f($_.nm-$_.ew-$_.el+$_.bar)+2,_28=$_.mw+1;_29>=_28;_29-=1){$k[$j++]=$f($_.nm-_29-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-3;$_.ncr();var _2E=$k[--$j];$_.lval=$f(_2E+$_.lval)}$_.sval=$f($_.sval-$_.lval*($_.el-$_.bar-1))}else{if($f($_.nm-$_.ew)>$_.mw){$_.sval=$_.sval-1}}$_.val=$f($_.val-$_.sval);if($_.val<0){break}$_.ew=$_.ew+1;$_.mask=$_.mask&~$bs(1,$_.bar)}$_.val=$f($_.val+$_.sval);$_.nm=$f($_.nm-$_.ew);$p($_.out,$_.bar,$_.ew)}$p($_.out,$_.el-1,$_.nm);$k[$j++]=$_.out};for(var _2f=0;_2f<=11;_2f+=1){$_.i=_2f;var _2g=$_.binval;var _2h=$_.i;$p(_2g,_2h+1,$f($g(_2g,_2h+1)+$g($_.binval,$_.i)%2013571*10));$p($_.binval,$_.i,~~($g($_.binval,$_.i)/2013571))}$_.d2=$g($_.binval,12)%2013571;var _2t=$_.binval;$p(_2t,12,~~($g(_2t,12)/2013571));$_.d1=0;$_.i=true;for(var _2v=0;_2v<=12;_2v+=1){$_.j=_2v;var _2y=$g($_.binval,$_.j);$k[$j++]=_2y;if(_2y==0&&$_.i){$j--}else{$_.i=false;$k[$j++]="d1";$k[$j++]=$_.d1;var _32=$k[$j-3];var _33=$k[$j-1];var _34=$k[$j-2];$j-=3;$_[_34]=$f(_33+_32*$pow(10,12-$_.j))}}$_.i=0;for(;;){if($_.d1<=$g($_.databarlimited_tab267,$_.i)){$q($G($_.databarlimited_tab267,$_.i+1,7));var _3A=$k[--$j];$_.d1te=_3A;var _3B=$k[--$j];$_.d1to=_3B;var _3C=$k[--$j];$_.d1mwe=_3C;var _3D=$k[--$j];$_.d1mwo=_3D;var _3E=$k[--$j];$_.d1ele=_3E;var _3F=$k[--$j];$_.d1elo=_3F;var _3G=$k[--$j];$_.d1gs=_3G;break}$_.i=$_.i+8}$_.i=0;for(;;){if($_.d2<=$g($_.databarlimited_tab267,$_.i)){$q($G($_.databarlimited_tab267,$_.i+1,7));var _3N=$k[--$j];$_.d2te=_3N;var _3O=$k[--$j];$_.d2to=_3O;var _3P=$k[--$j];$_.d2mwe=_3P;var _3Q=$k[--$j];$_.d2mwo=_3Q;var _3R=$k[--$j];$_.d2ele=_3R;var _3S=$k[--$j];$_.d2elo=_3S;var _3T=$k[--$j];$_.d2gs=_3T;break}$_.i=$_.i+8}$k[$j++]="d1wo";$k[$j++]=~~($f($_.d1-$_.d1gs)/$_.d1te);$k[$j++]=$_.d1elo;$k[$j++]=$_.d1mwo;$k[$j++]=7;$k[$j++]=false;$_.getRSSwidths();var _3a=$k[--$j];var _3b=$k[--$j];$_[_3b]=_3a;$k[$j++]="d1we";$k[$j++]=$f($_.d1-$_.d1gs)%$_.d1te;$k[$j++]=$_.d1ele;$k[$j++]=$_.d1mwe;$k[$j++]=7;$k[$j++]=true;$_.getRSSwidths();var _3h=$k[--$j];var _3i=$k[--$j];$_[_3i]=_3h;$k[$j++]="d2wo";$k[$j++]=~~($f($_.d2-$_.d2gs)/$_.d2te);$k[$j++]=$_.d2elo;$k[$j++]=$_.d2mwo;$k[$j++]=7;$k[$j++]=false;$_.getRSSwidths();var _3o=$k[--$j];var _3p=$k[--$j];$_[_3p]=_3o;$k[$j++]="d2we";$k[$j++]=$f($_.d2-$_.d2gs)%$_.d2te;$k[$j++]=$_.d2ele;$k[$j++]=$_.d2mwe;$k[$j++]=7;$k[$j++]=true;$_.getRSSwidths();var _3v=$k[--$j];var _3w=$k[--$j];$_[_3w]=_3v;$_.d1w=$a(14);for(var _3y=0;_3y<=6;_3y+=1){$_.i=_3y;$p($_.d1w,$_.i*2,$g($_.d1wo,$_.i));$p($_.d1w,$_.i*2+1,$g($_.d1we,$_.i))}$_.d2w=$a(14);for(var _4A=0;_4A<=6;_4A+=1){$_.i=_4A;$p($_.d2w,$_.i*2,$g($_.d2wo,$_.i));$p($_.d2w,$_.i*2+1,$g($_.d2we,$_.i))}$k[$j++]=Infinity;$q($_.d1w);$q($_.d2w);$_.widths=$a();$_.checksum=0;for(var _4N=0;_4N<=27;_4N+=1){$_.i=_4N;$_.checksum=$f($_.checksum+$g($_.widths,$_.i)*$g($_.databarlimited_checkweights,$_.i))}$_.checksum=$_.checksum%89;$_.seq=$g($_.checkseq,$_.checksum);$k[$j++]="swidths";$k[$j++]=~~($_.seq/21);$k[$j++]=8;$k[$j++]=3;$k[$j++]=6;$k[$j++]=false;$_.getRSSwidths();var _4Z=$k[--$j];var _4a=$k[--$j];$_[_4a]=_4Z;$k[$j++]="bwidths";$k[$j++]=$_.seq%21;$k[$j++]=8;$k[$j++]=3;$k[$j++]=6;$k[$j++]=false;$_.getRSSwidths();var _4c=$k[--$j];var _4d=$k[--$j];$_[_4d]=_4c;$_.checkwidths=$a([0,0,0,0,0,0,0,0,0,0,0,0,1,1]);for(var _4e=0;_4e<=5;_4e+=1){$_.i=_4e;$p($_.checkwidths,$_.i*2,$g($_.swidths,$_.i));$p($_.checkwidths,$_.i*2+1,$g($_.bwidths,$_.i))}$k[$j++]=Infinity;$k[$j++]=1;$q($_.d1w);$q($_.checkwidths);$q($_.d2w);$k[$j++]=1;$k[$j++]=1;$k[$j++]=5;$_.sbs=$a();if($_.height==-1){$_.height=10/72}$k[$j++]=Infinity;var _4t=$_.sbs;var _4y=[];for(var _4v=0,_4w=~~(($_.sbs.length+1)/2);_4v<_4w;_4v++){_4y[_4v]=$_.height}_4y=$a(_4y);var _52=[];for(var _50=0,_51=~~(($_.sbs.length+1)/2);_50<_51;_50++){_52[_50]=0}_52=$a(_52);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_4t;$k[$j++]="bhs";$k[$j++]=_4y;$k[$j++]="bbs";$k[$j++]=_52;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="borderleft";$k[$j++]=0;$k[$j++]="borderright";$k[$j++]=0;$k[$j++]="opt";$k[$j++]=$_.options;var _55=$d();$k[$j++]=_55;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_databarexpanded(){if(!bwipp_databarexpanded.globals){var $__=$_;$_=bwipp_databarexpanded.globals={};$_.databarexpanded_tab174=$a([347,0,12,5,7,2,87,4,1387,348,10,7,5,4,52,20,2947,1388,8,9,4,5,30,52,3987,2948,6,11,3,6,10,104,4191,3988,4,13,1,8,1,204]);$_.databarexpanded_finderwidths=$a([1,8,4,1,1,1,1,4,8,1,3,6,4,1,1,1,1,4,6,3,3,4,6,1,1,1,1,6,4,3,3,2,8,1,1,1,1,8,2,3,2,6,5,1,1,1,1,5,6,2,2,2,9,1,1,1,1,9,2,2]);$_.databarexpanded_finderseq=$a([$a([0,1]),$a([0,3,2]),$a([0,5,2,7]),$a([0,9,2,7,4]),$a([0,9,2,7,6,11]),$a([0,9,2,7,8,11,10]),$a([0,1,2,3,4,5,6,7]),$a([0,1,2,3,4,5,6,9,8]),$a([0,1,2,3,4,5,6,9,10,11]),$a([0,1,2,3,4,7,6,9,8,11,10])]);$_.databarexpanded_checkweights=$a([-1,-1,-1,-1,-1,-1,-1,-1,77,96,32,81,27,9,3,1,20,60,180,118,143,7,21,63,205,209,140,117,39,13,145,189,193,157,49,147,19,57,171,91,132,44,85,169,197,136,186,62,185,133,188,142,4,12,36,108,50,87,29,80,97,173,128,113,150,28,84,41,123,158,52,156,166,196,206,139,187,203,138,46,76,17,51,153,37,111,122,155,146,119,110,107,106,176,129,43,16,48,144,10,30,90,59,177,164,125,112,178,200,137,116,109,70,210,208,202,184,130,179,115,190,204,68,93,31,151,191,134,148,22,66,198,172,94,71,2,40,154,192,64,162,54,18,6,120,149,25,75,14,42,126,167,175,199,207,69,23,78,26,79,103,98,83,38,114,131,182,124,159,53,88,170,127,183,61,161,55,165,73,8,24,72,5,15,89,100,174,58,160,194,135,45]);$_.databarexpanded_fillpat=$a([0,0,1,0,0]);$_.databarexpanded_seppad=$a([0,0,0,0]);$_.databarexpanded_fnc1=-1;$_.databarexpanded_lnumeric=-2;$_.databarexpanded_lalphanumeric=-3;$_.databarexpanded_liso646=-4;$k[$j++]=Infinity;for(var _G=0;_G<=119;_G+=1){var _I=$Z($s(2),"00");var _K=$R($s(2),_G,11);$P(_I,2-_K.length,_K);$k[$j++]=_G;$k[$j++]=_I;if($g(_I,0)==65){var _M=$k[$j-1];$p(_M,0,94)}var _N=$k[$j-1];if($g(_N,1)==65){var _P=$k[$j-1];$p(_P,1,94)}var _Q=$k[--$j];var _R=$k[--$j];var _T=$Z($s(7),"0000000");var _V=$R($s(7),_R+8,2);$P(_T,7-_V.length,_V);$k[$j++]=_Q;$k[$j++]=_T}$k[$j++]=$_.databarexpanded_lalphanumeric;$k[$j++]="0000";var _W=$d();$_.numeric=_W;$k[$j++]=Infinity;for(var _X=48;_X<=57;_X+=1){var _Z=$Z($s(5),"00000");var _b=$R($s(5),_X-43,2);$P(_Z,5-_b.length,_b);$k[$j++]=_X;$k[$j++]=_Z}$k[$j++]=$_.databarexpanded_fnc1;$k[$j++]="01111";for(var _c=65;_c<=90;_c+=1){var _e=$Z($s(6),"000000");var _g=$R($s(6),_c-33,2);$P(_e,6-_g.length,_g);$k[$j++]=_c;$k[$j++]=_e}$k[$j++]=42;$k[$j++]="111010";for(var _h=44;_h<=47;_h+=1){var _j=$Z($s(6),"000000");var _l=$R($s(6),_h+15,2);$P(_j,6-_l.length,_l);$k[$j++]=_h;$k[$j++]=_j}$k[$j++]=$_.databarexpanded_lnumeric;$k[$j++]="000";$k[$j++]=$_.databarexpanded_liso646;$k[$j++]="00100";var _m=$d();$_.alphanumeric=_m;$k[$j++]=Infinity;for(var _n=48;_n<=57;_n+=1){var _p=$Z($s(5),"00000");var _r=$R($s(5),_n-43,2);$P(_p,5-_r.length,_r);$k[$j++]=_n;$k[$j++]=_p}$k[$j++]=$_.databarexpanded_fnc1;$k[$j++]="01111";for(var _s=65;_s<=90;_s+=1){var _u=$Z($s(7),"0000000");var _w=$R($s(7),_s-1,2);$P(_u,7-_w.length,_w);$k[$j++]=_s;$k[$j++]=_u}for(var _x=97;_x<=122;_x+=1){var _z=$Z($s(7),"0000000");var _11=$R($s(7),_x-7,2);$P(_z,7-_11.length,_11);$k[$j++]=_x;$k[$j++]=_z}$k[$j++]=33;$k[$j++]="11101000";$k[$j++]=34;$k[$j++]="11101001";for(var _12=37;_12<=47;_12+=1){var _14=$Z($s(8),"00000000");var _16=$R($s(8),_12+197,2);$P(_14,8-_16.length,_16);$k[$j++]=_12;$k[$j++]=_14}for(var _17=58;_17<=63;_17+=1){var _19=$Z($s(8),"00000000");var _1B=$R($s(8),_17+187,2);$P(_19,8-_1B.length,_1B);$k[$j++]=_17;$k[$j++]=_19}$k[$j++]=95;$k[$j++]="11111011";$k[$j++]=32;$k[$j++]="11111100";$k[$j++]=$_.databarexpanded_lnumeric;$k[$j++]="000";$k[$j++]=$_.databarexpanded_lalphanumeric;$k[$j++]="00100";var _1C=$d();$_.iso646=_1C;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarexpanded.globals);$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.dontdraw=false;$_.height=-1;$_.width=0;$_.format="expanded";$_.segments=-1;$_.linkage=false;$_.barxmult=34;$_.modunit=1;bwipp_processoptions();var _1D=$k[--$j];$_.options=_1D;var _1E=$k[--$j];$_.barcode=_1E;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fnc1=$_.databarexpanded_fnc1;if($_.barxmult<34){$k[$j++]="bwipp.databarStackedOmniBarXmult#13009";$k[$j++]="GS1 DataBar Expanded Stacked must have a barxmult of at least 34";bwipp_raiseerror()}if($ne($_.format,"expanded")&&$ne($_.format,"expandedstacked")){$k[$j++]="bwipp.databarexpandedBadFormat#13013";$k[$j++]="Valid formats are expanded and expandedstacked";bwipp_raiseerror()}if($_.segments==-1){var _1M=$eq($_.format,"expandedstacked")?4:22;$_.segments=_1M}else{if($_.segments<2||$_.segments>22||$_.segments%2!=0){$k[$j++]="bwipp.gs1databarexpandedBadSegments#13020";$k[$j++]="The number of segments must be even from 2 to 22";bwipp_raiseerror()}}$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _1R=$k[--$j];$_.fncs=_1R;var _1S=$k[--$j];$_.vals=_1S;var _1T=$k[--$j];$_.ais=_1T;$_.isNdigits=function(){var _1U=$k[--$j];var _1V=$k[$j-1];if(_1U!=_1V.length){$k[$j-1]=false}else{var _1W=$k[--$j];$k[$j++]=true;$F(_1W,function(){var _1X=$k[--$j];if(_1X<48||_1X>57){$k[$j-1]=false}})}};for(;;){if($_.ais.length==2){if($eq($g($_.ais,0),"01")&&$eq($g($_.ais,1),"3103")){$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();$k[$j++]=$g($_.vals,1);$k[$j++]=6;$_.isNdigits();var _1h=$k[--$j];var _1i=$k[--$j];if(_1i&&_1h){if($eq($G($g($_.vals,0),0,1),"9")&&$i($g($_.vals,1))<=32767){$k[$j++]="0100";$k[$j++]=false;break}}}}if($_.ais.length==2){if($eq($g($_.ais,0),"01")&&$eq($g($_.ais,1),"3202")){$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();$k[$j++]=$g($_.vals,1);$k[$j++]=6;$_.isNdigits();var _1x=$k[--$j];var _1y=$k[--$j];if(_1y&&_1x){if($eq($G($g($_.vals,0),0,1),"9")&&$i($g($_.vals,1))<=9999){$k[$j++]="0101";$k[$j++]=false;break}}}}if($_.ais.length==2){if($eq($g($_.ais,0),"01")&&$eq($g($_.ais,1),"3203")){$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();$k[$j++]=$g($_.vals,1);$k[$j++]=6;$_.isNdigits();var _2D=$k[--$j];var _2E=$k[--$j];if(_2E&&_2D){if($eq($G($g($_.vals,0),0,1),"9")&&$i($g($_.vals,1))<=22767){$k[$j++]="0101";$k[$j++]=false;break}}}}var _2L=$_.ais.length;if(_2L==2||_2L==3){var _2S=false;for(var _2M=3100;_2M<=3109;_2M+=1){var _2O=$R($s(4),_2M,10);_2S=_2S||$eq(_2O,$g($_.ais,1))}$_.ai310x=_2S;var _2a=false;for(var _2U=3200;_2U<=3209;_2U+=1){var _2W=$R($s(4),_2U,10);_2a=_2a||$eq(_2W,$g($_.ais,1))}$_.ai320x=_2a;if($_.ais.length==3){var _2d=$a(["11","13","15","17"]);var _2k=true;for(var _2e=0,_2f=_2d.length;_2e<_2f;_2e++){_2k=_2k&&$ne($g(_2d,_2e),$g($_.ais,2))}$_.aibad=_2k}else{$_.aibad=false}if($eq($g($_.ais,0),"01")&&($_.ai310x||$_.ai320x)&&!$_.aibad){if($_.ais.length==3){$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();$k[$j++]=$g($_.vals,1);$k[$j++]=6;$_.isNdigits();var _2w=$k[--$j];var _2x=$k[--$j];$k[$j++]=$an(_2x,_2w);$k[$j++]=$g($_.vals,2);$k[$j++]=6;$_.isNdigits();var _30=$k[--$j];var _31=$k[--$j];if(_31&&_30){var _34=$G($g($_.vals,0),0,1);var _3A=$i($G($g($_.vals,2),2,2));var _3E=$i($G($g($_.vals,2),4,2));$k[$j++]=$eq(_34,"9")&&$i($g($_.vals,1))<=99999&&(_3A>=1&&_3A<=12)&&(_3E>=0&&_3E<=31)}else{$k[$j++]=false}var _3F=$k[--$j];if(_3F){if($_.ai310x&&$eq($g($_.ais,2),"11")){$k[$j++]="0111000";$k[$j++]=false;break}if($_.ai320x&&$eq($g($_.ais,2),"11")){$k[$j++]="0111001";$k[$j++]=false;break}if($_.ai310x&&$eq($g($_.ais,2),"13")){$k[$j++]="0111010";$k[$j++]=false;break}if($_.ai320x&&$eq($g($_.ais,2),"13")){$k[$j++]="0111011";$k[$j++]=false;break}if($_.ai310x&&$eq($g($_.ais,2),"15")){$k[$j++]="0111100";$k[$j++]=false;break}if($_.ai320x&&$eq($g($_.ais,2),"15")){$k[$j++]="0111101";$k[$j++]=false;break}if($_.ai310x&&$eq($g($_.ais,2),"17")){$k[$j++]="0111110";$k[$j++]=false;break}if($_.ai320x&&$eq($g($_.ais,2),"17")){$k[$j++]="0111111";$k[$j++]=false;break}}}else{$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();$k[$j++]=$g($_.vals,1);$k[$j++]=6;$_.isNdigits();var _3i=$k[--$j];var _3j=$k[--$j];if(_3j&&_3i){if($eq($G($g($_.vals,0),0,1),"9")&&$i($g($_.vals,1))<=99999){if($_.ai310x){$k[$j++]="0111000";$k[$j++]=false;break}if($_.ai320x){$k[$j++]="0111001";$k[$j++]=false;break}}}}}}if($_.ais.length>=2){var _3y=false;for(var _3s=3920;_3s<=3923;_3s+=1){var _3u=$R($s(4),_3s,10);_3y=_3y||$eq(_3u,$g($_.ais,1))}$_.ai392x=_3y;$k[$j++]=$eq($g($_.ais,0),"01")&&$_.ai392x;$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();var _45=$k[--$j];var _46=$k[--$j];if(_46&&_45){if($eq($G($g($_.vals,0),0,1),"9")){$k[$j++]="01100";$k[$j++]=true;break}}}if($_.ais.length>=2){var _4H=false;for(var _4B=3930;_4B<=3933;_4B+=1){var _4D=$R($s(4),_4B,10);_4H=_4H||$eq(_4D,$g($_.ais,1))}$_.ai393x=_4H;$k[$j++]=$eq($g($_.ais,0),"01")&&$_.ai393x;$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();var _4O=$k[--$j];var _4P=$k[--$j];if(_4P&&_4O){$k[$j++]=$eq($G($g($_.vals,0),0,1),"9");if($g($_.vals,1).length>=3){$k[$j++]=$G($g($_.vals,1),0,3);$k[$j++]=3;$_.isNdigits()}else{$k[$j++]=false}var _4Y=$k[--$j];var _4Z=$k[--$j];if(_4Z&&_4Y){$k[$j++]="01101";$k[$j++]=true;break}}}$k[$j++]=$eq($g($_.ais,0),"01");$k[$j++]=$g($_.vals,0);$k[$j++]=14;$_.isNdigits();var _4e=$k[--$j];var _4f=$k[--$j];if(_4f&&_4e){$k[$j++]="1";$k[$j++]=true;break}$k[$j++]="00";$k[$j++]=true;break}var _4g=$k[--$j];$_.gpfallow=_4g;var _4h=$k[--$j];$_.method=_4h;$_.conv12to40=function(){var _4j=$Z($s(40),"0000000000000000000000000000000000000000");var _4k=$k[--$j];var _4o=$R($s(10),$i($G(_4k,0,3)),2);$P($G(_4j,0,10),10-_4o.length,_4o);var _4s=$R($s(10),$i($G(_4k,3,3)),2);$P($G(_4j,10,10),10-_4s.length,_4s);var _4w=$R($s(10),$i($G(_4k,6,3)),2);$P($G(_4j,20,10),10-_4w.length,_4w);var _50=$R($s(10),$i($G(_4k,9,3)),2);$P($G(_4j,30,10),10-_50.length,_50);$k[$j++]=_4j};$_.conv13to44=function(){var _52=$Z($s(44),"00000000000000000000000000000000000000000000");var _53=$k[--$j];var _57=$R($s(4),$i($G(_53,0,1)),2);$P($G(_52,0,4),4-_57.length,_57);$k[$j++]=_52;$k[$j++]=_52;$k[$j++]=$G(_53,1,12);$_.conv12to40();var _59=$k[--$j];var _5A=$k[--$j];$P(_5A,4,_59)};$_.tobin=function(){var _5B=$k[--$j];var _5C=$s(_5B);$k[$j++]=_5C;for(var _5E=0,_5D=_5C.length-1;_5E<=_5D;_5E+=1){var _5F=$k[$j-1];$p(_5F,_5E,48)}var _5G=$k[$j-1];$k[$j++]=_5G;var _5H=$k[$j-3];var _5I=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _5K=$R($s(_5I.length),_5H,2);$P(_5I,_5I.length-_5K.length,_5K)};if($eq($_.method,"00")){$_.cdf=$a([]);$_.gpf=$a([])}if($eq($_.method,"1")){var _5P=$G($g($_.vals,0),0,13);$k[$j++]="cdf";$k[$j++]=_5P;$_.conv13to44();var _5Q=$k[--$j];var _5R=$k[--$j];$_[_5R]=_5Q;$k[$j++]=Infinity;var _5S=$_.cdf;for(var _5T=0,_5U=_5S.length;_5T<_5U;_5T++){$k[$j++]=$g(_5S,_5T)-48}$_.cdf=$a();$_.gpf=$a([]);$_.ais=$G($_.ais,1,$_.ais.length-1);$_.vals=$G($_.vals,1,$_.vals.length-1);$_.fncs=$G($_.fncs,1,$_.fncs.length-1)}if($eq($_.method,"0100")){$_.cdf=$s(55);var _5k=$G($g($_.vals,0),1,12);$k[$j++]=$_.cdf;$k[$j++]=0;$k[$j++]=_5k;$_.conv12to40();var _5l=$k[--$j];var _5m=$k[--$j];var _5n=$k[--$j];$P(_5n,_5m,_5l);$k[$j++]=$_.cdf;$k[$j++]=40;$k[$j++]=$i($g($_.vals,1));$k[$j++]=15;$_.tobin();var _5r=$k[--$j];var _5s=$k[--$j];var _5t=$k[--$j];$P(_5t,_5s,_5r);$k[$j++]=Infinity;var _5u=$_.cdf;for(var _5v=0,_5w=_5u.length;_5v<_5w;_5v++){$k[$j++]=$g(_5u,_5v)-48}$_.cdf=$a();$_.gpf=$a([]);$_.ais=$a([]);$_.vals=$a([]);$_.fncs=$a([])}if($eq($_.method,"0101")){$_.cdf=$s(55);var _63=$G($g($_.vals,0),1,12);$k[$j++]=$_.cdf;$k[$j++]=0;$k[$j++]=_63;$_.conv12to40();var _64=$k[--$j];var _65=$k[--$j];var _66=$k[--$j];$P(_66,_65,_64);if($eq($g($_.ais,1),"3202")){$k[$j++]=$i($g($_.vals,1));$k[$j++]=15;$_.tobin()}else{$k[$j++]=$i($g($_.vals,1))+1e4;$k[$j++]=15;$_.tobin()}var _6E=$k[--$j];$P($_.cdf,40,_6E);$k[$j++]=Infinity;var _6F=$_.cdf;for(var _6G=0,_6H=_6F.length;_6G<_6H;_6G++){$k[$j++]=$g(_6F,_6G)-48}$_.cdf=$a();$_.gpf=$a([]);$_.ais=$a([]);$_.vals=$a([]);$_.fncs=$a([])}if($_.method.length==7){$_.cdf=$s(76);var _6O=$G($g($_.vals,0),1,12);$k[$j++]=$_.cdf;$k[$j++]=0;$k[$j++]=_6O;$_.conv12to40();var _6P=$k[--$j];var _6Q=$k[--$j];var _6R=$k[--$j];$P(_6R,_6Q,_6P);var _6S=$s(6);$P(_6S,0,$G($g($_.ais,1),3,1));$P(_6S,1,$G($g($_.vals,1),1,5));$k[$j++]=$i(_6S);$k[$j++]=20;$_.tobin();var _6a=$k[--$j];$P($_.cdf,40,_6a);if($_.ais.length==3){var _6d=$g($_.vals,2);$k[$j++]=$i($G(_6d,0,2))*384+(($i($G(_6d,2,2))-1)*32+$i($G(_6d,4,2)))}else{$k[$j++]=38400}$k[$j++]=16;$_.tobin();var _6i=$k[--$j];$P($_.cdf,60,_6i);$k[$j++]=Infinity;var _6j=$_.cdf;for(var _6k=0,_6l=_6j.length;_6k<_6l;_6k++){$k[$j++]=$g(_6j,_6k)-48}$_.cdf=$a();$_.gpf=$a([]);$_.ais=$a([]);$_.vals=$a([]);$_.fncs=$a([])}if($eq($_.method,"01100")){$_.cdf=$s(42);var _6s=$G($g($_.vals,0),1,12);$k[$j++]=$_.cdf;$k[$j++]=0;$k[$j++]=_6s;$_.conv12to40();var _6t=$k[--$j];var _6u=$k[--$j];var _6v=$k[--$j];$P(_6v,_6u,_6t);var _6z=$G($g($_.ais,1),3,1);$k[$j++]=$_.cdf;$k[$j++]=40;$k[$j++]=$i(_6z);$k[$j++]=2;$_.tobin();var _70=$k[--$j];var _71=$k[--$j];var _72=$k[--$j];$P(_72,_71,_70);$k[$j++]=Infinity;var _73=$_.cdf;for(var _74=0,_75=_73.length;_74<_75;_74++){$k[$j++]=$g(_73,_74)-48}$_.cdf=$a();$k[$j++]=Infinity;$F($g($_.vals,1));if($_.ais.length>2){$k[$j++]=$_.fnc1}$_.gpf=$a();$_.ais=$G($_.ais,2,$_.ais.length-2);$_.vals=$G($_.vals,2,$_.vals.length-2);$_.fncs=$G($_.fncs,2,$_.fncs.length-2)}if($eq($_.method,"01101")){$_.cdf=$s(52);var _7P=$G($g($_.vals,0),1,12);$k[$j++]=$_.cdf;$k[$j++]=0;$k[$j++]=_7P;$_.conv12to40();var _7Q=$k[--$j];var _7R=$k[--$j];var _7S=$k[--$j];$P(_7S,_7R,_7Q);var _7W=$G($g($_.ais,1),3,1);$k[$j++]=$_.cdf;$k[$j++]=40;$k[$j++]=$i(_7W);$k[$j++]=2;$_.tobin();var _7X=$k[--$j];var _7Y=$k[--$j];var _7Z=$k[--$j];$P(_7Z,_7Y,_7X);var _7d=$G($g($_.vals,1),0,3);$k[$j++]=$_.cdf;$k[$j++]=42;$k[$j++]=$i(_7d);$k[$j++]=10;$_.tobin();var _7e=$k[--$j];var _7f=$k[--$j];var _7g=$k[--$j];$P(_7g,_7f,_7e);$k[$j++]=Infinity;var _7h=$_.cdf;for(var _7i=0,_7j=_7h.length;_7i<_7j;_7i++){$k[$j++]=$g(_7h,_7i)-48}$_.cdf=$a();$k[$j++]=Infinity;var _7m=$g($_.vals,1);var _7n=$G(_7m,3,_7m.length-3);for(var _7o=0,_7p=_7n.length;_7o<_7p;_7o++){$k[$j++]=$g(_7n,_7o)}if($_.ais.length>2){$k[$j++]=$_.fnc1}$_.gpf=$a();$_.ais=$G($_.ais,2,$_.ais.length-2);$_.vals=$G($_.vals,2,$_.vals.length-2);$_.fncs=$G($_.fncs,2,$_.fncs.length-2)}if($_.gpfallow){$_.vlf=$a(2)}else{$_.vlf=$a([])}$k[$j++]=Infinity;$q($_.gpf);for(var _87=0,_86=$_.ais.length-1;_87<=_86;_87+=1){$_.i=_87;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){$k[$j++]=$_.fnc1}}$as($a($m()));var _8M=$k[--$j];$_.gpf=_8M;$j--;$_.rembits=function(){var _8N=$k[--$j];var _8O=48;var _8P=$ceil(_8N/12)*12;if($ceil(_8N/12)*12<48){var _=_8O;_8O=_8P;_8P=_}var _8Q=~~(_8P/12);$k[$j++]=_8N;$k[$j++]=_8P;$k[$j++]=_8Q;if(_8Q%$_.segments==1){var _8S=$k[--$j];$k[$j-1]=(_8S+1)*12}else{$j--}var _8U=$k[--$j];var _8V=$k[--$j];$k[$j++]=$f(_8U-_8V)};$_.encode=function(){var _8W=$k[$j-1];if($ne(_8W,"raw")){var _8X=$k[--$j];var _8Y=$k[--$j];$k[$j++]=$g(_8X,_8Y)}else{$j--}var _8a=$k[$j-1];if(_8a.length+$_.j>=252){$k[$j-1]="bwipp.gs1databarexpandedTooLong#13291";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}var _8d=$k[--$j];$k[$j++]=Infinity;$F(_8d,function(){$k[$j-1]-=48});var _8f=$a();$P($_.gpfenc,$_.j,_8f);$_.j=_8f.length+$_.j};$k[$j++]=Infinity;for(var _8k=0,_8l=$_.gpf.length;_8k<_8l;_8k++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=-1;$_.numericruns=$a();$k[$j++]=Infinity;for(var _8n=0,_8o=$_.gpf.length;_8n<_8o;_8n++){$k[$j++]=0}$k[$j++]=0;$_.alphanumericruns=$a();$k[$j++]=Infinity;for(var _8q=0,_8r=$_.gpf.length;_8q<_8r;_8q++){$k[$j++]=0}$k[$j++]=9999;$_.nextiso646only=$a();for(var _8t=$_.gpf.length-1;_8t>=0;_8t-=1){$_.i=_8t;var _8y=$Z($s(2),"00");var _91=$g($_.gpf,$_.i);$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=_8y;var _94=0;if(_91==$_.fnc1){_91=94}$p(_8y,_94,_91);if($_.i<$_.gpf.length-1){var _98=$k[$j-1];var _9B=$g($_.gpf,$_.i+1);var _9E=1;if(_9B==$_.fnc1){_9B=94}$p(_98,_9E,_9B)}var _9H=$k[--$j];if($has($_.numeric,_9H)){$p($_.numericruns,$_.i,$g($_.numericruns,$_.i+2)+2)}else{$p($_.numericruns,$_.i,0)}var _9Q=$k[$j-1];var _9S=$has($_.alphanumeric,_9Q);if(_9S){$p($_.alphanumericruns,$_.i,$g($_.alphanumericruns,$_.i+1)+1)}else{$p($_.alphanumericruns,$_.i,0)}var _9a=$k[--$j];if($has($_.iso646,_9a)&&!$has($_.alphanumeric,_9a)){$p($_.nextiso646only,$_.i,0)}else{$p($_.nextiso646only,$_.i,$g($_.nextiso646only,$_.i+1)+1)}}$_.gpfenc=$a(252);$_.i=0;$_.j=0;$_.mode="numeric";for(;;){if($_.i==$_.gpf.length){break}for(;;){if($eq($_.mode,"numeric")){if($_.i<=$_.gpf.length-2){var _9s=$s(2);var _9v=$g($_.gpf,$_.i);$k[$j++]=_9s;var _9y=0;if(_9v==$_.fnc1){_9v=94}$p(_9s,_9y,_9v);var _A0=$k[$j-1];var _A3=$g($_.gpf,$_.i+1);var _A6=1;if(_A3==$_.fnc1){_A3=94}$p(_A0,_A6,_A3);var _A8=$k[$j-1];var _AA=$has($_.numeric,_A8);if(_AA){$k[$j++]=$_.numeric;$_.encode();$_.i=$_.i+2;break}$k[$j-1]=$_.databarexpanded_lalphanumeric;$k[$j++]=$_.numeric;$_.encode();$_.mode="alphanumeric";break}else{var _AG=$g($_.gpf,$_.i);if(_AG<48||_AG>57){$k[$j++]=$_.databarexpanded_lalphanumeric;$k[$j++]=$_.numeric;$_.encode();$_.mode="alphanumeric";break}$k[$j++]="rem";$k[$j++]=12+1+$_.method.length+$_.vlf.length+$_.cdf.length+$_.j;$_.rembits();var _AM=$k[--$j];var _AN=$k[--$j];$_[_AN]=_AM;if($_.rem>=4&&$_.rem<=6){var _AT=$G($Z($s(6),"000000"),0,$_.rem);var _AY=$R($s(4),$g($_.gpf,$_.i)-47,2);$P(_AT,4-_AY.length,_AY);$k[$j++]=_AT;$k[$j++]="raw";$_.encode();$_.i=$_.i+1;break}else{var _Aa=$s(2);$p(_Aa,0,$g($_.gpf,$_.i));$p(_Aa,1,94);$k[$j++]=_Aa;$k[$j++]=$_.numeric;$_.encode();$_.i=$_.i+1;break}}}if($eq($_.mode,"alphanumeric")){if($g($_.gpf,$_.i)==$_.fnc1){$k[$j++]=$_.fnc1;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";$_.i=$_.i+1;break}var _Aq=$g($_.gpf,$_.i);if($has($_.iso646,_Aq)&&!$has($_.alphanumeric,_Aq)){$k[$j++]=$_.databarexpanded_liso646;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="iso646";break}if($g($_.numericruns,$_.i)>=6){$k[$j++]=$_.databarexpanded_lnumeric;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";break}var _B2=$g($_.numericruns,$_.i);if(_B2>=4&&$f(_B2+$_.i)==$_.gpf.length){$k[$j++]=$_.databarexpanded_lnumeric;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";break}$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=$_.alphanumeric;$_.encode();$_.i=$_.i+1;break}if($eq($_.mode,"iso646")){if($g($_.gpf,$_.i)==$_.fnc1){$k[$j++]=$_.fnc1;$k[$j++]=$_.iso646;$_.encode();$_.mode="numeric";$_.i=$_.i+1;break}if($g($_.numericruns,$_.i)>=4&&$g($_.nextiso646only,$_.i)>=10){$k[$j++]=$_.databarexpanded_lnumeric;$k[$j++]=$_.iso646;$_.encode();$_.mode="numeric";break}if($g($_.alphanumericruns,$_.i)>=5&&$g($_.nextiso646only,$_.i)>=10){$k[$j++]=$_.databarexpanded_lalphanumeric;$k[$j++]=$_.iso646;$_.encode();$_.mode="alphanumeric";break}$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=$_.iso646;$_.encode();$_.i=$_.i+1;break}}}$_.gpf=$G($_.gpfenc,0,$_.j);var _Bj=1+12+$_.method.length+$_.vlf.length+$_.cdf.length+$_.gpf.length;$k[$j++]=_Bj;$k[$j++]=_Bj;$_.rembits();var _Bk=$k[--$j];$_.pad=$a(_Bk);$k[$j++]=_Bk;if($_.vlf.length!=0){var _Bn=$k[--$j];var _Bo=$k[--$j];var _Bp=~~($f(_Bo+_Bn)/12);$p($_.vlf,0,_Bp%2);var _Br=_Bp<=14?0:1;$p($_.vlf,1,_Br)}else{$j-=2}if($_.pad.length>0){for(var _Bw=0,_Bv=$_.pad.length-1;_Bw<=_Bv;_Bw+=5){$_.i=_Bw;var _Bx=$_.pad;var _By=$_.i;var _Bz=$_.pad;var _C0=$_.i;var _C1=5;var _C2=_Bz.length-_C0;if(_Bz.length-_C0>5){var _=_C1;_C1=_C2;_C2=_}$P(_Bx,_By,$G($_.databarexpanded_fillpat,0,_C2))}if($eq($_.mode,"numeric")){$k[$j++]=Infinity;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.pad);var _C6=$a();$_.pad=$G(_C6,0,$_.pad.length)}}$k[$j++]=Infinity;$k[$j++]=$_.linkage?1:0;$F($_.method,function(){$k[$j-1]-=48});$q($_.vlf);$q($_.cdf);$q($_.gpf);$q($_.pad);$_.binval=$a();if($has($_.options,"debugbits")){$k[$j++]="bwipp.debugbits#13461";$k[$j++]=$_.binval;bwipp_raiseerror()}$_.datalen=~~($_.binval.length/12);$_.ncr=function(){var _CL=$k[--$j];var _CM=$k[--$j];var _CN=$f(_CM-_CL);if(_CL<$f(_CM-_CL)){var _=_CN;_CN=_CL;_CL=_}$k[$j++]=_CN;$k[$j++]=1;$k[$j++]=1;for(var _CP=_CM,_CO=_CL+1;_CP>=_CO;_CP-=1){var _CQ=$k[--$j];var _CR=$k[$j-1];var _CS=$k[$j-2];$k[$j++]=_CQ*_CP;if($le(_CR,_CS)){var _CT=$k[--$j];var _CU=$k[--$j];$k[$j++]=_CU+1;$k[$j++]=~~(_CT/_CU)}}for(;;){var _CW=$k[$j-2];var _CX=$k[$j-3];if($gt(_CW,_CX)){break}var _CY=$k[--$j];var _CZ=$k[--$j];$k[$j++]=_CZ+1;$k[$j++]=~~(_CY/_CZ)}var _Ca=$k[--$j];$j--;$k[$j-1]=_Ca};$_.getRSSwidths=function(){var _Ce=$k[--$j];$_.oe=_Ce;var _Cf=$k[--$j];$_.el=_Cf;var _Cg=$k[--$j];$_.mw=_Cg;var _Ch=$k[--$j];$_.nm=_Ch;var _Ci=$k[--$j];$_.val=_Ci;$_.out=$a($_.el);$_.mask=0;for(var _Cn=0,_Cm=$_.el-2;_Cn<=_Cm;_Cn+=1){$_.bar=_Cn;$_.ew=1;$_.mask=$_.mask|$bs(1,$_.bar);for(;;){$k[$j++]="sval";$k[$j++]=$f($_.nm-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _Cu=$k[--$j];var _Cv=$k[--$j];$_[_Cv]=_Cu;if($_.oe&&$_.mask==0&&$f($_.nm-$_.ew-$_.el*2+$_.bar*2)>=-2){$k[$j++]="sval";$k[$j++]=$_.sval;$k[$j++]=$f($_.nm-$_.ew-$_.el+$_.bar);$k[$j++]=$f($_.el-$_.bar)-2;$_.ncr();var _D9=$k[--$j];var _DA=$k[--$j];var _DB=$k[--$j];$_[_DB]=$f(_DA-_D9)}if($f($_.el-$_.bar)>2){$_.lval=0;for(var _DK=$f($_.nm-$_.ew-$_.el+$_.bar)+2,_DJ=$_.mw+1;_DK>=_DJ;_DK-=1){$k[$j++]=$f($_.nm-_DK-$_.ew)-1;$k[$j++]=$f($_.el-$_.bar)-3;$_.ncr();var _DP=$k[--$j];$_.lval=$f(_DP+$_.lval)}$_.sval=$f($_.sval-$_.lval*($_.el-$_.bar-1))}else{if($f($_.nm-$_.ew)>$_.mw){$_.sval=$_.sval-1}}$_.val=$f($_.val-$_.sval);if($_.val<0){break}$_.ew=$_.ew+1;$_.mask=$_.mask&~$bs(1,$_.bar)}$_.val=$f($_.val+$_.sval);$_.nm=$f($_.nm-$_.ew);$p($_.out,$_.bar,$_.ew)}$p($_.out,$_.el-1,$_.nm);$k[$j++]=$_.out};$_.dxw=$a($_.datalen);for(var _Du=0,_Dt=$_.datalen-1;_Du<=_Dt;_Du+=1){$_.x=_Du;$_.d=$G($_.binval,$_.x*12,12);var _E4=0;for(var _Dy=0;_Dy<=11;_Dy+=1){$_.j=_Dy;_E4=$f(_E4+$pow(2,11-$_.j)*$g($_.d,$_.j))}$_.d=_E4;$_.j=0;for(;;){if($le($_.d,$g($_.databarexpanded_tab174,$_.j))){$q($G($_.databarexpanded_tab174,$_.j+1,7));var _EB=$k[--$j];$_.dte=_EB;var _EC=$k[--$j];$_.dto=_EC;var _ED=$k[--$j];$_.dmwe=_ED;var _EE=$k[--$j];$_.dmwo=_EE;var _EF=$k[--$j];$_.dele=_EF;var _EG=$k[--$j];$_.delo=_EG;var _EH=$k[--$j];$_.dgs=_EH;break}$_.j=$_.j+8}$k[$j++]="dwo";$k[$j++]=~~($f($_.d-$_.dgs)/$_.dte);$k[$j++]=$_.delo;$k[$j++]=$_.dmwo;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _EO=$k[--$j];var _EP=$k[--$j];$_[_EP]=_EO;$k[$j++]="dwe";$k[$j++]=$f($_.d-$_.dgs)%$_.dte;$k[$j++]=$_.dele;$k[$j++]=$_.dmwe;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _EV=$k[--$j];var _EW=$k[--$j];$_[_EW]=_EV;$_.dw=$a(8);if($_.x%2==0){for(var _EZ=0;_EZ<=3;_EZ+=1){$_.j=_EZ;$p($_.dw,7-$_.j*2,$g($_.dwo,$_.j));$p($_.dw,6-$_.j*2,$g($_.dwe,$_.j))}}else{for(var _Ek=0;_Ek<=3;_Ek+=1){$_.j=_Ek;$p($_.dw,$_.j*2,$g($_.dwo,$_.j));$p($_.dw,$_.j*2+1,$g($_.dwe,$_.j))}}$p($_.dxw,$_.x,$_.dw)}$_.seq=$g($_.databarexpanded_finderseq,~~(($_.datalen-2)/2));$_.fxw=$a($_.seq.length);for(var _F4=0,_F3=$_.seq.length-1;_F4<=_F3;_F4+=1){$_.x=_F4;$p($_.fxw,$_.x,$G($_.databarexpanded_finderwidths,$g($_.seq,$_.x)*5,5))}$k[$j++]=Infinity;$F($_.seq,function(){var _FC=$k[--$j];$q($G($_.databarexpanded_checkweights,_FC*16,16))});var _FE=$a();$_.checkweightseq=$G(_FE,8,_FE.length-8);$k[$j++]=Infinity;var _FG=$_.dxw;for(var _FH=0,_FI=_FG.length;_FH<_FI;_FH++){$q($g(_FG,_FH))}$_.widths=$a();$_.checksum=0;for(var _FM=0,_FL=$_.widths.length-1;_FM<=_FL;_FM+=1){$_.i=_FM;$_.checksum=$f($_.checksum+$g($_.widths,$_.i)*$g($_.checkweightseq,$_.i))}$_.checksum=$f($_.checksum%211+($_.datalen-3)*211);$_.i=0;for(;;){if($_.checksum<=$g($_.databarexpanded_tab174,$_.i)){$q($G($_.databarexpanded_tab174,$_.i+1,7));var _Fb=$k[--$j];$_.cte=_Fb;var _Fc=$k[--$j];$_.cto=_Fc;var _Fd=$k[--$j];$_.cmwe=_Fd;var _Fe=$k[--$j];$_.cmwo=_Fe;var _Ff=$k[--$j];$_.cele=_Ff;var _Fg=$k[--$j];$_.celo=_Fg;var _Fh=$k[--$j];$_.cgs=_Fh;break}$_.i=$_.i+8}$k[$j++]="cwo";$k[$j++]=~~($f($_.checksum-$_.cgs)/$_.cte);$k[$j++]=$_.celo;$k[$j++]=$_.cmwo;$k[$j++]=4;$k[$j++]=true;$_.getRSSwidths();var _Fo=$k[--$j];var _Fp=$k[--$j];$_[_Fp]=_Fo;$k[$j++]="cwe";$k[$j++]=$f($_.checksum-$_.cgs)%$_.cte;$k[$j++]=$_.cele;$k[$j++]=$_.cmwe;$k[$j++]=4;$k[$j++]=false;$_.getRSSwidths();var _Fv=$k[--$j];var _Fw=$k[--$j];$_[_Fw]=_Fv;$_.cw=$a(8);for(var _Fy=0;_Fy<=3;_Fy+=1){$_.i=_Fy;$p($_.cw,$_.i*2,$g($_.cwo,$_.i));$p($_.cw,$_.i*2+1,$g($_.cwe,$_.i))}var _G9=$a(22);$p(_G9,0,$_.cw);$P(_G9,1,$_.dxw);$_.dxw=$G(_G9,0,$_.datalen+1);$_.datalen=$_.dxw.length;$_.rows=$a($ceil($_.datalen/$_.segments));$_.numrows=$_.rows.length;for(var _GL=0,_GK=$_.numrows-1;_GL<=_GK;_GL+=1){$_.r=_GL;$k[$j++]=Infinity;if($_.segments%4!=0&&$_.r%2==1){$k[$j++]=0}$k[$j++]=1;$k[$j++]=1;for(var _GQ=0,_GP=$_.segments-1;_GQ<=_GP;_GQ+=1){$_.pos=_GQ+$_.r*$_.segments;if($_.pos<$_.datalen){$q($g($_.dxw,$_.pos));if($_.pos%2==0){$q($g($_.fxw,~~($_.pos/2)))}}}var _Gc=$m()+2;var _Gd=$a(_Gc);$k[$j++]=1;$k[$j++]=1;$as(_Gd);var _Gf=$k[--$j];$p($_.rows,$_.r,_Gf);$j--}if($ne($_.format,"expandedstacked")){var _Gj=$g($_.rows,0);$_.sbs=$G(_Gj,1,_Gj.length-1);$k[$j++]=Infinity;$k[$j++]=1;for(var _Gn=0,_Gm=$_.datalen-1;_Gn<=_Gm;_Gn+=1){$_.i=_Gn;$q($g($_.dxw,$_.i));if($_.i%2==0){$q($g($_.fxw,~~($_.i/2)))}}$k[$j++]=1;$k[$j++]=1;$_.sbs=$a();if($_.height==-1){$_.height=34/72}$k[$j++]=Infinity;var _Gw=$_.sbs;var _H1=[];for(var _Gy=0,_Gz=~~(($_.sbs.length+1)/2);_Gy<_Gz;_Gy++){_H1[_Gy]=$_.height}_H1=$a(_H1);var _H5=[];for(var _H3=0,_H4=~~(($_.sbs.length+1)/2);_H3<_H4;_H3++){_H5[_H3]=0}_H5=$a(_H5);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_Gw;$k[$j++]="bhs";$k[$j++]=_H1;$k[$j++]="bbs";$k[$j++]=_H5;$k[$j++]="borderleft";$k[$j++]=0;$k[$j++]="borderright";$k[$j++]=0;$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=0;$k[$j++]="opt";$k[$j++]=$_.options;var _H7=$d();$k[$j++]=_H7;if($_._render){bwipp_renlinear()}}else{$_.seps=$a($_.numrows);for(var _HD=0,_HC=$_.numrows-1;_HD<=_HC;_HD+=1){$_.r=_HD;$_.row=$g($_.rows,$_.r);$k[$j++]=Infinity;for(var _HJ=0,_HI=$_.row.length-1;_HJ<=_HI;_HJ+=2){$_.i=_HJ;for(var _HN=0,_HO=$g($_.row,$_.i);_HN<_HO;_HN++){$k[$j++]=0}if($_.i<$_.row.length-1){for(var _HU=0,_HV=$g($_.row,$_.i+1);_HU<_HV;_HU++){$k[$j++]=1}}}$as($a($m()));var _HY=$k[--$j];$_.row=_HY;$k[$j-1]=Infinity;$F($_.row,function(){var _Ha=$k[--$j];$k[$j++]=1-_Ha});$_.sep=$a();$k[$j++]=Infinity;for(var _Hd=19,_Hc=$_.row.length-13;_Hd<=_Hc;_Hd+=98){$k[$j++]=_Hd}for(var _Hg=68,_Hf=$_.row.length-13;_Hg<=_Hf;_Hg+=98){$k[$j++]=_Hg}$_.finderpos=$a();var _Hh=$_.finderpos;for(var _Hi=0,_Hj=_Hh.length;_Hi<_Hj;_Hi++){var _Hk=$g(_Hh,_Hi);for(var _Hm=_Hk,_Hl=_Hk+14;_Hm<=_Hl;_Hm+=1){$_.i=_Hm;if($g($_.row,$_.i)==0){if($g($_.row,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _Hy=$k[--$j];$p($_.sep,$_.i,_Hy)}}$P($_.sep,0,$_.databarexpanded_seppad);$P($_.sep,$_.row.length-4,$_.databarexpanded_seppad);if($_.segments%4==0&&$_.r%2==1){if($g($_.rows,$_.r).length!=$g($_.rows,0).length&&$_.finderpos.length%2==1){$k[$j++]=Infinity;$k[$j++]=0;$q($_.row);$_.row=$a();$k[$j++]=Infinity;$k[$j++]=0;$q($_.sep);$_.sep=$a()}else{for(var _IE=$_.row.length-1;_IE>=0;_IE-=1){$k[$j++]=$g($_.row,_IE)}$as($_.row);$j--;for(var _IJ=$_.sep.length-1;_IJ>=0;_IJ-=1){$k[$j++]=$g($_.sep,_IJ)}$as($_.sep);$j--}}$p($_.rows,$_.r,$_.row);$p($_.seps,$_.r,$_.sep)}$_.pixx=$g($_.rows,0).length;var _IY=[];for(var _IW=0,_IX=$_.pixx;_IW<_IX;_IW++){_IY[_IW]=0}_IY=$a(_IY);$P(_IY,0,$g($_.rows,$_.numrows-1));$p($_.rows,$_.numrows-1,_IY);var _Ih=[];for(var _If=0,_Ig=$_.pixx;_If<_Ig;_If++){_Ih[_If]=0}_Ih=$a(_Ih);$P(_Ih,0,$g($_.seps,$_.numrows-1));$p($_.seps,$_.numrows-1,_Ih);$k[$j++]=Infinity;for(var _Io=0,_Ip=~~($_.pixx/2)+1;_Io<_Ip;_Io++){$k[$j++]=0;$k[$j++]=1}var _Iq=$a();$_.sep=$G(_Iq,0,$_.pixx);$P($_.sep,0,$_.databarexpanded_seppad);$P($_.sep,$_.pixx-4,$_.databarexpanded_seppad);$k[$j++]=Infinity;for(var _Iy=0,_Ix=$_.numrows-1;_Iy<=_Ix;_Iy+=1){$_.r=_Iy;if($_.r!=0){$q($g($_.seps,$_.r))}$q($g($_.rows,$_.r));if($_.r!=$_.numrows-1){$q($g($_.seps,$_.r));$q($_.sep)}}$_.pixs=$a();$k[$j++]=Infinity;for(var _JE=0,_JD=$_.numrows-1;_JE<=_JD;_JE+=1){$_.r=_JE;if($_.r!=0){$k[$j++]=1}$k[$j++]=$_.barxmult;if($_.r!=$_.numrows-1){$k[$j++]=1;$k[$j++]=1}}$_.rowmult=$a();var _JJ=$_.rowmult;var _JO=0;for(var _JK=0,_JL=_JJ.length;_JK<_JL;_JK++){_JO=$f(_JO+$g(_JJ,_JK))}$_.pixy=_JO;$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",0],["borderright",0],["bordertop",0],["borderbottom",0],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}}$_=$__}function bwipp_databarexpandedstacked(){var $__=$_;$_=Object.assign({},$_,bwipp_databarexpandedstacked.globals);$_.dontdraw=false;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","expandedstacked");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_databarexpanded();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1northamericancoupon(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1northamericancoupon.globals);$_.dontdraw=false;$_.includetext=false;$_.coupontextfont="OCR-B";$_.coupontextsize=9;$_.coupontextxoffset=null;$_.coupontextyoffset=null;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();$j--;var _5=$k[--$j];$_.vals=_5;var _6=$k[--$j];$_.ais=_6;if($_.ais.length!=1){$k[$j++]="bwipp.gs1northamericancouponBadAIStructure#13974";$k[$j++]="A GS1 North American Coupon should consist of a single AI (8110)";bwipp_raiseerror()}if($ne($g($_.ais,0),"8110")){$k[$j++]="bwipp.gs1northamericancouponBadAIStructure#13977";$k[$j++]="A GS1 North American Coupon should consist of a single AI (8110)";bwipp_raiseerror()}$_.val=$g($_.vals,0);$_.vli=$g($_.val,0)-48;if($_.vli<0||$_.vli>6){$k[$j++]="bwipp.gs1northamericancouponBadVLI#13984";$k[$j++]="The AI (8110) data should start with a Company Prefix length indicator in the range 0 to 6";bwipp_raiseerror()}if($_.val.length<$_.vli+13){$k[$j++]="bwipp.gs1northamericancouponTooShort#13987";$k[$j++]="The AI (8110) data is too short";bwipp_raiseerror()}$_.gcp=$G($_.val,1,$_.vli+6);$_.cod=$G($_.val,$_.vli+7,6);$_.coupontext=$s($_.gcp.length+7);$P($_.coupontext,0,$_.gcp);$P($_.coupontext,$_.gcp.length,"-");$P($_.coupontext,$_.gcp.length+1,$_.cod);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_databarexpandedstacked();var _Z=$k[--$j];$_.args=_Z;if($_.includetext){if($_.coupontextxoffset==null){$_.coupontextxoffset=0}if($_.coupontextyoffset==null){$_.coupontextyoffset=$g($_.args,"height")*72+3}if($has($_.args,"txt")){$_.txt=$g($_.args,"txt");$_.newtxt=$a($_.txt.length+1);$P($_.newtxt,0,$_.txt);$p($_.newtxt,$_.newtxt.length-1,$a([$_.coupontext,$_.coupontextxoffset,$_.coupontextyoffset,$_.coupontextfont,$_.coupontextsize]));$p($_.args,"txt",$_.newtxt)}else{$p($_.args,"txt",$a([$a([$_.coupontext,$_.coupontextxoffset,$_.coupontextyoffset,$_.coupontextfont,$_.coupontextsize])]))}}$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_pharmacode(){var $__=$_;$_=Object.assign({},$_,bwipp_pharmacode.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.nwidth=1;$_.wwidth=3;$_.swidth=2;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length<1||$_.barcode.length>6){$k[$j++]="bwipp.pharmacodeBadLength#14109";$k[$j++]="Pharmacode must be 1 to 6 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.pharmacodeBadCharacter#14113";$k[$j++]="Pharmacode must contain only digits";bwipp_raiseerror()}});var _9=$i($_.barcode);if(_9<3||_9>131070){$k[$j++]="bwipp.pharmacodeBadValue#14117";$k[$j++]="Pharmacode value must be between 3 and 131070";bwipp_raiseerror()}$_.txt=$a($_.barcode.length);for(var _E=0,_D=$_.barcode.length-1;_E<=_D;_E+=1){$_.i=_E;$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$_.barcode=$R($s(17),$i($_.barcode)+1,2);$_.barcode=$G($_.barcode,1,$_.barcode.length-1);$_.barlen=$_.barcode.length;$_.sbs=$a($_.barlen*2);for(var _W=0,_V=$_.barlen-1;_W<=_V;_W+=1){$_.i=_W;$_.enc=$G($_.barcode,$_.i,1);if($eq($_.enc,"0")){$p($_.sbs,$_.i*2,$_.nwidth)}else{$p($_.sbs,$_.i*2,$_.wwidth)}$p($_.sbs,$_.i*2+1,$_.swidth)}if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _l=$_.sbs;var _q=[];for(var _n=0,_o=~~(($_.sbs.length+1)/2);_n<_o;_n++){_q[_n]=$_.height}_q=$a(_q);var _u=[];for(var _s=0,_t=~~(($_.sbs.length+1)/2);_s<_t;_s++){_u[_s]=0}_u=$a(_u);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_l;$k[$j++]="bhs";$k[$j++]=_q;$k[$j++]="bbs";$k[$j++]=_u;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="opt";$k[$j++]=$_.options;var _x=$d();$k[$j++]=_x;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_pharmacode2(){if(!bwipp_pharmacode2.globals){var $__=$_;$_=bwipp_pharmacode2.globals={};$_.pharmacode2_base3sub=$a([3,1,2]);$_.pharmacode2_base3map=$a([2,0,1]);$_.pharmacode2_htmult=$a([1,1,2]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_pharmacode2.globals);$_.dontdraw=false;$_.includetext=false;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _3=$k[--$j];$_.options=_3;var _4=$k[--$j];$_.barcode=_4;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length<1||$_.barcode.length>8){$k[$j++]="bwipp.pharmacode2BadLength#14247";$k[$j++]="Two-track Pharmacode must be 1 to 6 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _A=$k[--$j];if(_A<48||_A>57){$k[$j++]="bwipp.pharmacode2badCharacter#14251";$k[$j++]="Two-track Pharmacode must contain only digits";bwipp_raiseerror()}});var _C=$i($_.barcode);if(_C<4||_C>64570080){$k[$j++]="bwipp.pharmacode2badValue#14255";$k[$j++]="Two-track Pharmacode value must be between 4 and 64570080";bwipp_raiseerror()}if($_.height==-1){$_.height=4}$_.txt=$a($_.barcode.length);for(var _I=0,_H=$_.barcode.length-1;_I<=_H;_I+=1){$_.i=_I;$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$_.encstr=$s(16);$_.sbs=$a(32);$_.bar=1/25.4*$_.height;$_.spc=1/25.4*72;$_.i=15;$_.val=$i($_.barcode);for(;;){if($_.val==0){break}var _V=$_.val%3;$_.val=~~($f($_.val-$g($_.pharmacode2_base3sub,_V))/3);$p($_.encstr,$_.i,$g($_.pharmacode2_base3map,_V));$_.i=$_.i-1}$_.encstr=$G($_.encstr,$_.i+1,15-$_.i);$_.bhs=$a($_.encstr.length);$_.bbs=$a($_.encstr.length);for(var _m=0,_l=$_.encstr.length-1;_m<=_l;_m+=1){$_.i=_m;var _p=$g($_.encstr,$_.i);$p($_.bhs,$_.i,$_.bar*$g($_.pharmacode2_htmult,_p));$p($_.bbs,$_.i,$g($a([0,$_.bar,0]),_p))}$k[$j++]=Infinity;var _z=$_.bhs;var _10=$_.bbs;var _15=[];for(var _12=0,_13=$_.encstr.length*2;_12<_13;_12++){_15[_12]=$_.spc}_15=$a(_15);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bhs";$k[$j++]=_z;$k[$j++]="bbs";$k[$j++]=_10;$k[$j++]="sbs";$k[$j++]=_15;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="text1yoffset";$k[$j++]=-4;$k[$j++]="opt";$k[$j++]=$_.options;var _18=$d();$k[$j++]=_18;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code2of5(){if(!bwipp_code2of5.globals){var $__=$_;$_=bwipp_code2of5.globals={};var _5=new Map([["industrial",$a(["1111313111","3111111131","1131111131","3131111111","1111311131","3111311111","1131311111","1111113131","3111113111","1131113111","313111","31113"])],["iata",$a(["1111313111","3111111131","1131111131","3131111111","1111311131","3111311111","1131311111","1111113131","3111113111","1131113111","1111","311"])],["matrix",$a(["113311","311131","131131","331111","113131","313111","133111","111331","311311","131311","311111","31111"])],["coop",$a(["331111","111331","113131","113311","131131","131311","133111","311131","311311","313111","3131","133"])],["datalogic",$a(["113311","311131","131131","331111","113131","313111","133111","111331","311311","131311","1111","311"])]]);$_.code2of5_versions=_5;$_.code2of5_barchars="0123456789";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code2of5.globals);$_.dontdraw=false;$_.includecheck=false;$_.validatecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.version="industrial";$_.modunit=1;bwipp_processoptions();var _6=$k[--$j];$_.options=_6;var _7=$k[--$j];$_.barcode=_7;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.code2of5emptyData#14423";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.code2of5inputTooLong#14427";$k[$j++]="The input data is too long";bwipp_raiseerror()}$F($_.barcode,function(){var _D=$k[--$j];if(_D<48||_D>57){$k[$j++]="bwipp.code2of5badCharacter#14435";$k[$j++]="Code 25 must contain only digits";bwipp_raiseerror()}});var _H=$_.barcode.length;if($_.validatecheck){_H=_H-1}$_.barlen=_H;$_.checksum=0;for(var _L=0,_K=$_.barlen-1;_L<=_K;_L+=1){$_.i=_L;var _U=$_.checksum;var _T=$g($_.barcode,$_.i)-48;if($f($_.barlen-$_.i)%2!=0){_T=_T*3}$_.checksum=$f(_U+_T)}$_.checksum=(10-$_.checksum%10)%10;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$_.checksum+48){$k[$j++]="bwipp.code2of5badCheckDigit#14455";$k[$j++]="Incorrect Code 25 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen);$_.includecheck=true}if(!$has($_.code2of5_versions,$_.version)){$k[$j++]="bwipp.code2of5badVersion#14462";$k[$j++]="Unrecognised Code 25 version";bwipp_raiseerror()}$_.encs=$g($_.code2of5_versions,$_.version);$_.cs=$g($_.encs,0).length;$k[$j++]="cw";var _p=0;$F($g($_.encs,0),function(){var _n=$k[--$j];_p=$f(_p+(_n-48))});var _q=$k[--$j];$_[_q]=_p;$_.ss=$g($_.encs,10).length;$k[$j++]="sw";var _x=0;$F($g($_.encs,10),function(){var _v=$k[--$j];_x=$f(_x+(_v-48))});var _y=$k[--$j];$_[_y]=_x;$_.es=$g($_.encs,11).length;var _15=$_.barlen;if($_.includecheck){_15=_15+1}var _18=$s($f(_15*$_.cs+$_.ss+$_.es));$_.sbs=_18;$P($_.sbs,0,$g($_.encs,10));for(var _1F=0,_1E=$_.barlen-1;_1F<=_1E;_1F+=1){$_.i=_1F;$X($_.code2of5_barchars,$G($_.barcode,$_.i,1));$j--;var _1J=$k[--$j];$_.indx=_1J.length;$j-=2;$_.enc=$g($_.encs,$_.indx);$P($_.sbs,$_.i*$_.cs+$_.ss,$_.enc)}if($_.includecheck){$P($_.sbs,$f($_.barlen*$_.cs+$_.ss),$g($_.encs,$_.checksum));$P($_.sbs,$f($_.barlen*$_.cs+$_.cs+$_.ss),$g($_.encs,11))}else{$P($_.sbs,$f($_.barlen*$_.cs+$_.ss),$g($_.encs,11))}$k[$j++]="text";if($_.includecheck&&$_.includecheckintext){var _1q=$s($_.barlen+1);$P(_1q,0,$_.barcode);$P(_1q,$_.barlen,$G($_.code2of5_barchars,$_.checksum,1));$k[$j++]=_1q}else{$k[$j++]=$_.barcode}var _1w=$k[--$j];var _1x=$k[--$j];$_[_1x]=_1w;$k[$j++]=Infinity;var _1y=$_.sbs;for(var _1z=0,_20=_1y.length;_1z<_20;_1z++){$k[$j++]=$g(_1y,_1z)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _23=$_.sbs;var _28=[];for(var _25=0,_26=~~(($_.sbs.length+1)/2);_25<_26;_25++){_28[_25]=$_.height}_28=$a(_28);var _2C=[];for(var _2A=0,_2B=~~(($_.sbs.length+1)/2);_2A<_2B;_2A++){_2C[_2A]=0}_2C=$a(_2C);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_23;$k[$j++]="bhs";$k[$j++]=_28;$k[$j++]="bbs";$k[$j++]=_2C;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2K=$d();$k[$j++]=_2K;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_industrial2of5(){var $__=$_;$_=Object.assign({},$_,bwipp_industrial2of5.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"version","industrial");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code2of5();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_iata2of5(){var $__=$_;$_=Object.assign({},$_,bwipp_iata2of5.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"version","iata");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code2of5();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_matrix2of5(){var $__=$_;$_=Object.assign({},$_,bwipp_matrix2of5.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"version","matrix");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code2of5();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_coop2of5(){var $__=$_;$_=Object.assign({},$_,bwipp_coop2of5.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"version","coop");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code2of5();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_datalogic2of5(){var $__=$_;$_=Object.assign({},$_,bwipp_datalogic2of5.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"version","datalogic");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code2of5();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_code11(){if(!bwipp_code11.globals){var $__=$_;$_=bwipp_code11.globals={};$_.code11_encs=$a(["111131","311131","131131","331111","113131","313111","133111","111331","311311","311111","113111","113311"]);$_.code11_barchars="0123456789-";$_.charvals=new Map;for(var _1=0;_1<=10;_1+=1){$p($_.charvals,$G($_.code11_barchars,_1,1),_1)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code11.globals);$_.dontdraw=false;$_.includecheck=false;$_.validatecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _5=$k[--$j];$_.options=_5;var _6=$k[--$j];$_.barcode=_6;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.code11emptyData#14935";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.code11inputTooLong#14939";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _D=0,_C=$_.barcode.length-1;_D<=_C;_D+=1){if(!$has($_.charvals,$G($_.barcode,_D,1))){$k[$j++]="bwipp.code11badCharacter#14947";$k[$j++]="Code 11 must contain only digits and dashes";bwipp_raiseerror()}}$_.barlen=$_.barcode.length;if($_.validatecheck){if($_.barlen==11){$k[$j++]="bwipp.code11badLength#14960";$k[$j++]="Code 11 cannot be 11 characters using check digits";bwipp_raiseerror()}var _N=$_.barlen<=10?1:2;$_.barlen=$_.barlen-_N}var _S=$_.includecheck||$_.validatecheck?$_.barlen>=10?2:1:0;$_.numchecks=_S;$_.checksum1=0;$_.checksum2=0;for(var _W=0,_V=$_.barlen-1;_W<=_V;_W+=1){$_.i=_W;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$_.checksum1=$f($_.checksum1+(($_.barlen-$_.i-1)%10+1)*$_.indx);$_.checksum2=$f($_.checksum2+(($_.barlen-$_.i)%9+1)*$_.indx)}$_.checksum1=$_.checksum1%11;$_.checksum2=$f($_.checksum2+$_.checksum1)%11;if($_.validatecheck){if($_.numchecks==1){if($g($_.barcode,$_.barlen)!=$g($_.code11_barchars,$_.checksum1)){$k[$j++]="bwipp.code11badCheckDigit#14978";$k[$j++]="Incorrect Code 11 check digit provided";bwipp_raiseerror()}}else{if($g($_.barcode,$_.barlen)!=$g($_.code11_barchars,$_.checksum1)||$g($_.barcode,$_.barlen+1)!=$g($_.code11_barchars,$_.checksum2)){$k[$j++]="bwipp.code11badCheckDigits#14983";$k[$j++]="Incorrect Code 11 check digits provided";bwipp_raiseerror()}}$_.barcode=$G($_.barcode,0,$_.barlen);$_.includecheck=true}$_.sbs=$s($f($_.barlen+$_.numchecks)*6+12);$P($_.sbs,0,$g($_.code11_encs,11));for(var _1E=0,_1D=$_.barlen-1;_1E<=_1D;_1E+=1){$_.i=_1E;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$_.enc=$g($_.code11_encs,$_.indx);$P($_.sbs,$_.i*6+6,$_.enc)}if($_.includecheck){if($_.barlen>=10){$P($_.sbs,$_.barlen*6+6,$g($_.code11_encs,$_.checksum1));$P($_.sbs,$_.barlen*6+12,$g($_.code11_encs,$_.checksum2));$P($_.sbs,$_.barlen*6+18,$g($_.code11_encs,11))}else{$P($_.sbs,$_.barlen*6+6,$g($_.code11_encs,$_.checksum1));$P($_.sbs,$_.barlen*6+12,$g($_.code11_encs,11))}}else{$P($_.sbs,$_.barlen*6+6,$g($_.code11_encs,11))}$k[$j++]="text";if($_.includecheck&&$_.includecheckintext){if($_.barlen>=10){var _1q=$s($_.barlen+2);$P(_1q,0,$_.barcode);$P(_1q,$_.barlen,$G($_.code11_barchars,$_.checksum1,1));$P(_1q,$_.barlen+1,$G($_.code11_barchars,$_.checksum2,1));$k[$j++]=_1q}else{var _1z=$s($_.barlen+1);$P(_1z,0,$_.barcode);$P(_1z,$_.barlen,$G($_.code11_barchars,$_.checksum1,1));$k[$j++]=_1z}}else{$k[$j++]=$_.barcode}var _25=$k[--$j];var _26=$k[--$j];$_[_26]=_25;$k[$j++]=Infinity;var _27=$_.sbs;for(var _28=0,_29=_27.length;_28<_29;_28++){$k[$j++]=$g(_27,_28)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _2C=$_.sbs;var _2H=[];for(var _2E=0,_2F=~~(($_.sbs.length+1)/2);_2E<_2F;_2E++){_2H[_2E]=$_.height}_2H=$a(_2H);var _2L=[];for(var _2J=0,_2K=~~(($_.sbs.length+1)/2);_2J<_2K;_2J++){_2L[_2J]=0}_2L=$a(_2L);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_2C;$k[$j++]="bhs";$k[$j++]=_2H;$k[$j++]="bbs";$k[$j++]=_2L;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2T=$d();$k[$j++]=_2T;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_bc412(){if(!bwipp_bc412.globals){var $__=$_;$_=bwipp_bc412.globals={};$_.bc412_barchars="0R9GLVHA8EZ4NTS1J2Q6C7DYKBUIX3FWP5M";$_.bc412_encs=$a(["11111115","13111212","11131113","12111213","12121311","13131111","12111312","11131212","11121411","11151111","15111111","11111511","12131211","13121112","13111311","11111214","12121113","11111313","13111113","11121213","11141112","11121312","11141211","14121111","12121212","11131311","13121211","12111411","14111211","11111412","12111114","14111112","12141111","11121114","12131112","12","111"]);$_.charvals=new Map;for(var _1=0;_1<=34;_1+=1){$p($_.charvals,$G($_.bc412_barchars,_1,1),_1)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_bc412.globals);$_.dontdraw=false;$_.includecheck=false;$_.validatecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.includestartstop=false;$_.semi=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _5=$k[--$j];$_.options=_5;var _6=$k[--$j];$_.barcode=_6;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.bc412emptyData#15170";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.bc412inputTooLong#15174";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.semi){$_.includestartstop=true}for(var _E=0,_D=$_.barcode.length-1;_E<=_D;_E+=1){if(!$has($_.charvals,$G($_.barcode,_E,1))){$k[$j++]="bwipp.bc412badCharacter#15187";$k[$j++]="BC412 must contain only digits and capital letters except O";bwipp_raiseerror()}}if($_.semi){if($_.validatecheck){if($_.barcode.length<8||$_.barcode.length>19){$k[$j++]="bwipp.bc412semivalidateBadLength#15197";$k[$j++]="BC412 semi with check digit must be 8 to 19 characters long";bwipp_raiseerror()}}else{if($_.barcode.length<7||$_.barcode.length>18){$k[$j++]="bwipp.bc412semiBadLength#15201";$k[$j++]="BC412 semi must be 7 to 18 characters long";bwipp_raiseerror()}}$_.barlen=$_.barcode.length;if(!$_.validatecheck){$_.barlen=$_.barlen+1;$_.sbarcode=$s($_.barlen);$p($_.sbarcode,0,$g($_.barcode,0));$p($_.sbarcode,1,48);$P($_.sbarcode,2,$G($_.barcode,1,$_.barlen-2));$_.barcode=$_.sbarcode}$_.sumodd=0;$_.sumeven=0;for(var _f=0,_e=$_.barlen-1;_f<=_e;_f+=1){$_.i=_f;$k[$j++]=$g($_.charvals,$G($_.barcode,$_.i,1));if($_.i%2==0){var _n=$k[--$j];$_.sumodd=$f(_n+$_.sumodd)}else{var _p=$k[--$j];$_.sumeven=$f(_p+$_.sumeven)}}$_.checksum=$f($_.sumodd%35+$_.sumeven%35*2)%35;if($_.validatecheck){if($_.checksum!=0){$k[$j++]="bwipp.bc412semiBadCheckDigit#15227";$k[$j++]="Incorrect BC412 semi check digit provided";bwipp_raiseerror()}}else{$_.checksum=$_.checksum*17%35;$p($_.barcode,1,$g($_.bc412_barchars,$_.checksum))}}else{var _11=$_.barcode.length;if($_.validatecheck){_11=_11-1}$_.barlen=_11;$_.checksum=0;for(var _15=0,_14=$_.barlen-1;_15<=_14;_15+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_15,1))+$_.checksum)}$_.checksum=$_.checksum%35;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.bc412_barchars,$_.checksum)){$k[$j++]="bwipp.bc412badCheckDigit#15244";$k[$j++]="Incorrect BC412 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen);$_.includecheck=true}}$_.sbs=$s(($_.barlen+1)*8+5);if($_.includestartstop){$P($_.sbs,0,$g($_.bc412_encs,35));$_.pos=2}else{$_.pos=0}for(var _1S=0,_1R=$_.barlen-1;_1S<=_1R;_1S+=1){$_.i=_1S;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.sbs,$_.pos,$g($_.bc412_encs,$_.indx));$_.pos=$_.pos+8}if($_.includecheck){$P($_.sbs,$_.pos,$g($_.bc412_encs,$_.checksum));$_.pos=$_.pos+8}if($_.includestartstop){$P($_.sbs,$_.pos,$g($_.bc412_encs,36));$_.pos=$_.pos+3}$k[$j++]="text";if($_.includecheck&&$_.includecheckintext){var _1r=$s($_.barlen+1);$P(_1r,0,$_.barcode);$P(_1r,$_.barlen,$G($_.bc412_barchars,$_.checksum,1));$k[$j++]=_1r}else{$k[$j++]=$_.barcode}var _1x=$k[--$j];var _1y=$k[--$j];$_[_1y]=_1x;$_.sbs=$G($_.sbs,0,$_.pos);if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _27=[];var _23=$_.sbs;for(var _24=0,_25=_23.length;_24<_25;_24++){_27[_24]=$g(_23,_24)-48}_27=$a(_27);var _2C=[];for(var _29=0,_2A=~~(($_.sbs.length+1)/2);_29<_2A;_29++){_2C[_29]=$_.height}_2C=$a(_2C);var _2G=[];for(var _2E=0,_2F=~~(($_.sbs.length+1)/2);_2E<_2F;_2E++){_2G[_2E]=0}_2G=$a(_2G);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_27;$k[$j++]="bhs";$k[$j++]=_2C;$k[$j++]="bbs";$k[$j++]=_2G;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2O=$d();$k[$j++]=_2O;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_rationalizedCodabar(){if(!bwipp_rationalizedCodabar.globals){var $__=$_;$_=bwipp_rationalizedCodabar.globals={};$_.rationalizedCodabar_encs=$a(["11111331","11113311","11131131","33111111","11311311","31111311","13111131","13113111","13311111","31131111","11133111","11331111","31113131","31311131","31313111","11313131","11331311","13131131","11131331","11133311"]);$_.rationalizedCodabar_barchars="0123456789-$:/.+ABCD";$_.rationalizedCodabar_barcharsalt="0123456789-$:/.+TN*E";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_rationalizedCodabar.globals);$_.dontdraw=false;$_.altstartstop=false;$_.includecheck=false;$_.validatecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.rationalizedCodabarinputTooLong#15397";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length<2||$_.barcode.length<3&&$_.validatecheck){$k[$j++]="bwipp.rationalizedCodabarBadLength#15406";$k[$j++]="Codabar must be at least 2 characters in length excluding any check digit";bwipp_raiseerror()}var _A=$_.altstartstop?$_.rationalizedCodabar_barcharsalt:$_.rationalizedCodabar_barchars;$_.barchars=_A;$_.charvals=new Map;for(var _C=0;_C<=19;_C+=1){$p($_.charvals,$G($_.barchars,_C,1),_C)}$_.bodyvals=new Map;for(var _G=0;_G<=15;_G+=1){$p($_.bodyvals,$G($_.barchars,_G,1),_G)}$_.ssvals=new Map;for(var _K=16;_K<=19;_K+=1){$p($_.ssvals,$G($_.barchars,_K,1),_K)}if(!$has($_.ssvals,$G($_.barcode,0,1))||!$has($_.ssvals,$G($_.barcode,$_.barcode.length-1,1))){if($_.altstartstop){$k[$j++]="bwipp.rationalizedCodabarBadAltStartStop#15424";$k[$j++]="Codabar start and stop characters must be one of E N T or *";bwipp_raiseerror()}else{$k[$j++]="bwipp.rationalizedCodabarBadStartStop#15426";$k[$j++]="Codabar start and stop characters must be one of A B C or D";bwipp_raiseerror()}}for(var _a=1,_Z=$_.barcode.length-2;_a<=_Z;_a+=1){if(!$has($_.bodyvals,$G($_.barcode,_a,1))){$k[$j++]="bwipp.rationalizedCodabarBadCharacter#15431";$k[$j++]="Codabar body must contain only digits and symbols - $ : / . +";bwipp_raiseerror()}}var _i=$_.barcode.length;if($_.validatecheck){_i=_i-1}$_.barlen=_i;$_.checksum=0;for(var _m=0,_l=$_.barlen-2;_m<=_l;_m+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_m,1))+$_.checksum)}$_.checksum=$f($g($_.charvals,$G($_.barcode,$_.barcode.length-1,1))+$_.checksum);$_.checksum=(16-$_.checksum%16)%16;if($_.validatecheck){if($ne($g($_.barcode,$_.barlen-1),$g($_.barchars,$_.checksum))){$k[$j++]="bwipp.rationalizedCodabarBadCheckDigit#15453";$k[$j++]="Incorrect Codabar check digit provided";bwipp_raiseerror()}var _17=$s($_.barlen);$P(_17,0,$G($_.barcode,0,$_.barlen-1));$P(_17,$_.barlen-1,$G($_.barcode,$_.barlen,1));$_.barcode=_17;$_.includecheck=true}var _1I=$_.barlen;if($_.includecheck){_1I=_1I+1}var _1J=$s(_1I*8);$_.sbs=_1J;for(var _1N=0,_1M=$_.barlen-2;_1N<=_1M;_1N+=1){$_.i=_1N;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$_.enc=$g($_.rationalizedCodabar_encs,$_.indx);$P($_.sbs,$_.i*8,$_.enc)}if($_.includecheck){$P($_.sbs,$_.barlen*8-8,$g($_.rationalizedCodabar_encs,$_.checksum));$_.indx=$g($_.charvals,$G($_.barcode,$_.barlen-1,1));$_.enc=$g($_.rationalizedCodabar_encs,$_.indx);$P($_.sbs,$_.barlen*8,$_.enc)}else{$_.indx=$g($_.charvals,$G($_.barcode,$_.barlen-1,1));$_.enc=$g($_.rationalizedCodabar_encs,$_.indx);$P($_.sbs,$_.barlen*8-8,$_.enc)}$k[$j++]="text";if($_.includecheck&&$_.includecheckintext){var _20=$s($_.barlen+1);$P(_20,0,$G($_.barcode,0,$_.barlen-1));$P(_20,$_.barlen-1,$G($_.barchars,$_.checksum,1));$P(_20,$_.barlen,$G($_.barcode,$_.barlen-1,1));$k[$j++]=_20}else{$k[$j++]=$_.barcode}var _2D=$k[--$j];var _2E=$k[--$j];$_[_2E]=_2D;if($_.height==-1){$_.height=1}$k[$j++]=Infinity;$k[$j++]=Infinity;$F($_.sbs,function(){$k[$j-1]-=48});var _2I=$a();var _2N=[];for(var _2K=0,_2L=~~(($_.sbs.length+1)/2);_2K<_2L;_2K++){_2N[_2K]=$_.height}_2N=$a(_2N);var _2R=[];for(var _2P=0,_2Q=~~(($_.sbs.length+1)/2);_2P<_2Q;_2P++){_2R[_2P]=0}_2R=$a(_2R);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_2I;$k[$j++]="bhs";$k[$j++]=_2N;$k[$j++]="bbs";$k[$j++]=_2R;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2Z=$d();$k[$j++]=_2Z;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_onecode(){if(!bwipp_onecode.globals){var $__=$_;$_=bwipp_onecode.globals={};var _4=new Map([[20,$a([0])],[25,$a([1])],[29,$a([1,0,0,0,0,1])],[31,$a([1,0,0,0,1,0,0,0,0,1])]]);$_.onecode_startvals=_4;$_.onecode_tab513=$a([31,7936,47,7808,55,7552,59,7040,61,6016,62,3968,79,7744,87,7488,91,6976,93,5952,94,3904,103,7360,107,6848,109,5824,110,3776,115,6592,117,5568,118,3520,121,5056,122,3008,124,1984,143,7712,151,7456,155,6944,157,5920,158,3872,167,7328,171,6816,173,5792,174,3744,179,6560,181,5536,182,3488,185,5024,186,2976,188,1952,199,7264,203,6752,205,5728,206,3680,211,6496,213,5472,214,3424,217,4960,218,2912,220,1888,227,6368,229,5344,230,3296,233,4832,234,2784,236,1760,241,4576,242,2528,244,1504,248,992,271,7696,279,7440,283,6928,285,5904,286,3856,295,7312,299,6800,301,5776,302,3728,307,6544,309,5520,310,3472,313,5008,314,2960,316,1936,327,7248,331,6736,333,5712,334,3664,339,6480,341,5456,342,3408,345,4944,346,2896,348,1872,355,6352,357,5328,358,3280,361,4816,362,2768,364,1744,369,4560,370,2512,372,1488,376,976,391,7216,395,6704,397,5680,398,3632,403,6448,405,5424,406,3376,409,4912,410,2864,412,1840,419,6320,421,5296,422,3248,425,4784,426,2736,428,1712,433,4528,434,2480,436,1456,440,944,451,6256,453,5232,454,3184,457,4720,458,2672,460,1648,465,4464,466,2416,468,1392,472,880,481,4336,482,2288,484,1264,488,752,527,7688,535,7432,539,6920,541,5896,542,3848,551,7304,555,6792,557,5768,558,3720,563,6536,565,5512,566,3464,569,5e3,570,2952,572,1928,583,7240,587,6728,589,5704,590,3656,595,6472,597,5448,598,3400,601,4936,602,2888,604,1864,611,6344,613,5320,614,3272,617,4808,618,2760,620,1736,625,4552,626,2504,628,1480,632,968,647,7208,651,6696,653,5672,654,3624,659,6440,661,5416,662,3368,665,4904,666,2856,668,1832,675,6312,677,5288,678,3240,681,4776,682,2728,684,1704,689,4520,690,2472,692,1448,696,936,707,6248,709,5224,710,3176,713,4712,714,2664,716,1640,721,4456,722,2408,724,1384,728,872,737,4328,738,2280,740,1256,775,7192,779,6680,781,5656,782,3608,787,6424,789,5400,790,3352,793,4888,794,2840,796,1816,803,6296,805,5272,806,3224,809,4760,810,2712,812,1688,817,4504,818,2456,820,1432,824,920,835,6232,837,5208,838,3160,841,4696,842,2648,844,1624,849,4440,850,2392,852,1368,865,4312,866,2264,868,1240,899,6200,901,5176,902,3128,905,4664,906,2616,908,1592,913,4408,914,2360,916,1336,929,4280,930,2232,932,1208,961,4216,962,2168,964,1144,1039,7684,1047,7428,1051,6916,1053,5892,1054,3844,1063,7300,1067,6788,1069,5764,1070,3716,1075,6532,1077,5508,1078,3460,1081,4996,1082,2948,1084,1924,1095,7236,1099,6724,1101,5700,1102,3652,1107,6468,1109,5444,1110,3396,1113,4932,1114,2884,1116,1860,1123,6340,1125,5316,1126,3268,1129,4804,1130,2756,1132,1732,1137,4548,1138,2500,1140,1476,1159,7204,1163,6692,1165,5668,1166,3620,1171,6436,1173,5412,1174,3364,1177,4900,1178,2852,1180,1828,1187,6308,1189,5284,1190,3236,1193,4772,1194,2724,1196,1700,1201,4516,1202,2468,1204,1444,1219,6244,1221,5220,1222,3172,1225,4708,1226,2660,1228,1636,1233,4452,1234,2404,1236,1380,1249,4324,1250,2276,1287,7188,1291,6676,1293,5652,1294,3604,1299,6420,1301,5396,1302,3348,1305,4884,1306,2836,1308,1812,1315,6292,1317,5268,1318,3220,1321,4756,1322,2708,1324,1684,1329,4500,1330,2452,1332,1428,1347,6228,1349,5204,1350,3156,1353,4692,1354,2644,1356,1620,1361,4436,1362,2388,1377,4308,1378,2260,1411,6196,1413,5172,1414,3124,1417,4660,1418,2612,1420,1588,1425,4404,1426,2356,1441,4276,1442,2228,1473,4212,1474,2164,1543,7180,1547,6668,1549,5644,1550,3596,1555,6412,1557,5388,1558,3340,1561,4876,1562,2828,1564,1804,1571,6284,1573,5260,1574,3212,1577,4748,1578,2700,1580,1676,1585,4492,1586,2444,1603,6220,1605,5196,1606,3148,1609,4684,1610,2636,1617,4428,1618,2380,1633,4300,1634,2252,1667,6188,1669,5164,1670,3116,1673,4652,1674,2604,1681,4396,1682,2348,1697,4268,1698,2220,1729,4204,1730,2156,1795,6172,1797,5148,1798,3100,1801,4636,1802,2588,1809,4380,1810,2332,1825,4252,1826,2204,1857,4188,1858,2140,1921,4156,1922,2108,2063,7682,2071,7426,2075,6914,2077,5890,2078,3842,2087,7298,2091,6786,2093,5762,2094,3714,2099,6530,2101,5506,2102,3458,2105,4994,2106,2946,2119,7234,2123,6722,2125,5698,2126,3650,2131,6466,2133,5442,2134,3394,2137,4930,2138,2882,2147,6338,2149,5314,2150,3266,2153,4802,2154,2754,2161,4546,2162,2498,2183,7202,2187,6690,2189,5666,2190,3618,2195,6434,2197,5410,2198,3362,2201,4898,2202,2850,2211,6306,2213,5282,2214,3234,2217,4770,2218,2722,2225,4514,2226,2466,2243,6242,2245,5218,2246,3170,2249,4706,2250,2658,2257,4450,2258,2402,2273,4322,2311,7186,2315,6674,2317,5650,2318,3602,2323,6418,2325,5394,2326,3346,2329,4882,2330,2834,2339,6290,2341,5266,2342,3218,2345,4754,2346,2706,2353,4498,2354,2450,2371,6226,2373,5202,2374,3154,2377,4690,2378,2642,2385,4434,2401,4306,2435,6194,2437,5170,2438,3122,2441,4658,2442,2610,2449,4402,2465,4274,2497,4210,2567,7178,2571,6666,2573,5642,2574,3594,2579,6410,2581,5386,2582,3338,2585,4874,2586,2826,2595,6282,2597,5258,2598,3210,2601,4746,2602,2698,2609,4490,2627,6218,2629,5194,2630,3146,2633,4682,2641,4426,2657,4298,2691,6186,2693,5162,2694,3114,2697,4650,2705,4394,2721,4266,2753,4202,2819,6170,2821,5146,2822,3098,2825,4634,2833,4378,2849,4250,2881,4186,2945,4154,3079,7174,3083,6662,3085,5638,3086,3590,3091,6406,3093,5382,3094,3334,3097,4870,3107,6278,3109,5254,3110,3206,3113,4742,3121,4486,3139,6214,3141,5190,3145,4678,3153,4422,3169,4294,3203,6182,3205,5158,3209,4646,3217,4390,3233,4262,3265,4198,3331,6166,3333,5142,3337,4630,3345,4374,3361,4246,3393,4182,3457,4150,3587,6158,3589,5134,3593,4622,3601,4366,3617,4238,3649,4174,3713,4142,3841,4126,4111,7681,4119,7425,4123,6913,4125,5889,4135,7297,4139,6785,4141,5761,4147,6529,4149,5505,4153,4993,4167,7233,4171,6721,4173,5697,4179,6465,4181,5441,4185,4929,4195,6337,4197,5313,4201,4801,4209,4545,4231,7201,4235,6689,4237,5665,4243,6433,4245,5409,4249,4897,4259,6305,4261,5281,4265,4769,4273,4513,4291,6241,4293,5217,4297,4705,4305,4449,4359,7185,4363,6673,4365,5649,4371,6417,4373,5393,4377,4881,4387,6289,4389,5265,4393,4753,4401,4497,4419,6225,4421,5201,4425,4689,4483,6193,4485,5169,4489,4657,4615,7177,4619,6665,4621,5641,4627,6409,4629,5385,4633,4873,4643,6281,4645,5257,4649,4745,4675,6217,4677,5193,4739,6185,4741,5161,4867,6169,4869,5145,5127,7173,5131,6661,5133,5637,5139,6405,5141,5381,5155,6277,5157,5253,5187,6213,5251,6181,5379,6165,5635,6157,6151,7171,6155,6659,6163,6403,6179,6275,6211,5189,4681,4433,4321,3142,2634,2386,2274,1612,1364,1252,856,744,496]);$_.onecode_tab213=$a([3,6144,5,5120,6,3072,9,4608,10,2560,12,1536,17,4352,18,2304,20,1280,24,768,33,4224,34,2176,36,1152,40,640,48,384,65,4160,66,2112,68,1088,72,576,80,320,96,192,129,4128,130,2080,132,1056,136,544,144,288,257,4112,258,2064,260,1040,264,528,513,4104,514,2056,516,1032,1025,4100,1026,2052,2049,4098,4097,2050,1028,520,272,160]);$_.onecode_barmap=$a([7,2,4,3,1,10,0,0,9,12,2,8,5,5,6,11,8,9,3,1,0,1,5,12,2,5,1,8,4,4,9,11,6,3,8,10,3,9,7,6,5,11,1,4,8,5,2,12,9,10,0,2,7,1,6,7,3,6,4,9,0,3,8,6,6,4,2,7,1,1,9,9,7,10,5,2,4,0,3,8,6,2,0,4,8,11,1,0,9,8,3,12,2,6,7,7,5,1,4,10,1,12,6,9,7,3,8,0,5,8,9,7,4,6,2,10,3,4,0,5,8,4,5,7,7,11,1,9,6,0,9,6,0,6,4,8,2,1,3,2,5,9,8,12,4,11,6,1,9,5,7,4,3,3,1,2,0,7,2,0,1,3,4,1,6,10,3,5,8,7,9,4,2,11,5,6,0,8,7,12,4,2,8,1,5,10,3,0,9,3,0,9,6,5,2,4,7,8,1,7,5,0,4,5,2,3,0,10,6,12,9,2,3,11,1,6,8,8,7,9,5,4,0,11,1,5,2,2,9,1,4,12,8,3,6,6,7,0,3,7,4,7,7,5,0,12,1,11,2,9,9,0,6,8,5,3,3,10,8,2]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_onecode.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _8=$k[--$j];$_.options=_8;var _9=$k[--$j];$_.barcode=_9;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.barlen=$_.barcode.length;$F($_.barcode,function(){var _E=$k[--$j];if(_E<48||_E>57){$k[$j++]="bwipp.onecodeBadCharacter#15703";$k[$j++]="The data must contain only digits";bwipp_raiseerror()}});if($_.barlen!=20&&$_.barlen!=25&&$_.barlen!=29&&$_.barlen!=31){$k[$j++]="bwipp.onecodeInvalidLength#15708";$k[$j++]="The data must be 20, 25, 29 or 31 digits";bwipp_raiseerror()}$k[$j++]=Infinity;var _L=$eq($G($_.barcode,5,1),"9")?14:11;$k[$j++]=2;$k[$j++]=2;$k[$j++]=5;$k[$j++]=5;$k[$j++]=_L;$k[$j++]=_L;$k[$j++]=20;$k[$j++]=20;$k[$j++]=25;$k[$j++]=25;$k[$j++]=29;$k[$j++]=29;$_.txtdict=$d();$_.txt=$a($_.barlen+6);$_.spacecnt=0;for(var _Q=0,_P=$_.barlen-1;_Q<=_P;_Q+=1){$_.i=_Q;if($has($_.txtdict,$_.i)){$p($_.txt,$_.i+$_.spacecnt,$a([" ",0,0,"",0]));$_.spacecnt=$_.spacecnt+1}$p($_.txt,$_.i+$_.spacecnt,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$_.txt=$G($_.txt,0,$_.barcode.length+$_.spacecnt);$_.normalize=function(){var _k=$k[--$j];$_.base=_k;var _l=$k[--$j];$_.num=_l;for(var _n=$_.num.length-1;_n>=1;_n-=1){$_.i=_n;var _o=$_.num;var _p=$_.i;$p(_o,_p-1,$f($g(_o,_p-1)+~~($g($_.num,$_.i)/$_.base)));$p($_.num,$_.i,$g($_.num,$_.i)%$_.base)}if($ge($g($_.num,0),$_.base)){$k[$j++]=Infinity;$k[$j++]=$g($_.num,0);for(;;){var _16=$k[$j-1];if($lt(_16,$_.base)){break}var _18=$k[--$j];$k[$j++]=_18%$_.base;$k[$j++]=~~(_18/$_.base)}$_.extcnt=$m()-1;$as($a($m()));var _1E=$k[--$j];$_.extdigits=_1E;$k[$j-1]=Infinity;for(var _1G=$_.extcnt;_1G>=0;_1G-=1){$k[$j++]=$g($_.extdigits,_1G)}$q($G($_.num,1,$_.num.length-1));$_.num=$a()}$k[$j++]=0;for(;;){var _1M=$k[$j-1];if(_1M>=$_.num.length){break}var _1P=$k[$j-1];if($g($_.num,_1P)!=0){break}$k[$j-1]+=1}var _1S=$k[$j-1];if(_1S>=$_.num.length){$k[$j-1]=$a([0])}else{var _1W=$k[--$j];$k[$j++]=$G($_.num,_1W,$f($_.num.length-_1W))}};$_.bigadd=function(){var _1Z=$k[--$j];var _1a=$k[--$j];$_.offset=$abs(_1Z.length-_1a.length);if(_1Z.length<_1a.length){var _=_1Z;_1Z=_1a;_1a=_}$_.a=_1Z;$_.b=_1a;for(var _1d=0,_1c=$_.b.length-1;_1d<=_1c;_1d+=1){var _1e=$_.a;var _1f=$_.offset;$p(_1e,_1d+_1f,$f($g(_1e,_1d+_1f)+$g($_.b,_1d)))}$k[$j++]=$_.a};$k[$j++]="binval";$k[$j++]=$g($_.onecode_startvals,$_.barlen);var _1s=[];var _1o=$G($_.barcode,20,$_.barlen-20);for(var _1p=0,_1q=_1o.length;_1p<_1q;_1p++){_1s[_1p]=$g(_1o,_1p)-48}_1s=$a(_1s);$k[$j++]=_1s;$_.bigadd();var _1t=$k[--$j];var _1u=$k[--$j];$_[_1u]=_1t;$k[$j++]=Infinity;$q($_.binval);$k[$j++]=$g($_.barcode,0)-48;$_.binval=$a();var _22=[];var _1y=$_.binval;for(var _1z=0,_20=_1y.length;_1z<_20;_1z++){_22[_1z]=$g(_1y,_1z)*5}_22=$a(_22);$k[$j++]="binval";$k[$j++]=_22;$k[$j++]=$a([$g($_.barcode,1)-48]);$_.bigadd();$k[$j++]=10;$_.normalize();var _26=$k[--$j];var _27=$k[--$j];$_[_27]=_26;$k[$j++]=Infinity;$q($_.binval);var _2A=$G($_.barcode,2,18);for(var _2B=0,_2C=_2A.length;_2B<_2C;_2B++){$k[$j++]=$g(_2A,_2B)-48}$_.binval=$a();$_.bytes=$a(13);var _2F=$_.binval;$_.bintmp=$A($a(_2F.length),_2F);for(var _2I=12;_2I>=0;_2I-=1){$_.i=_2I;for(var _2L=0,_2K=$_.bintmp.length-2;_2L<=_2K;_2L+=1){$_.j=_2L;var _2M=$_.bintmp;var _2N=$_.j;$p(_2M,_2N+1,$f($g(_2M,_2N+1)+$g($_.bintmp,$_.j)%256*10));$p($_.bintmp,$_.j,~~($g($_.bintmp,$_.j)/256))}$p($_.bytes,$_.i,$g($_.bintmp,$_.bintmp.length-1)%256);var _2c=$_.bintmp;var _2d=$_.bintmp;$p(_2c,_2d.length-1,~~($g(_2c,_2d.length-1)/256))}$_.fcs=2047;$_.dat=$g($_.bytes,0)*32;for(var _2h=0;_2h<6;_2h++){var _2m=$_.fcs*2;if((($_.fcs^$_.dat)&1024)!=0){_2m=_2m^3893}$_.fcs=_2m&2047;$_.dat=$_.dat*2}for(var _2p=1;_2p<=12;_2p+=1){$_.dat=$g($_.bytes,_2p)*8;for(var _2s=0;_2s<8;_2s++){var _2x=$_.fcs*2;if((($_.fcs^$_.dat)&1024)!=0){_2x=_2x^3893}$_.fcs=_2x&2047;$_.dat=$_.dat*2}}$_.codewords=$a(10);for(var _31=9;_31>=0;_31-=1){$_.i=_31;var _33=$_.i==9?636:1365;$_.b=_33;for(var _36=0,_35=$_.binval.length-2;_36<=_35;_36+=1){$_.j=_36;var _37=$_.binval;var _38=$_.j;$p(_37,_38+1,$f($g(_37,_38+1)+$g($_.binval,$_.j)%$_.b*10));$p($_.binval,$_.j,~~($g($_.binval,$_.j)/$_.b))}$p($_.codewords,$_.i,$g($_.binval,$_.binval.length-1)%$_.b);var _3Q=$_.binval;var _3R=$_.binval;$p(_3Q,_3R.length-1,~~($g(_3Q,_3R.length-1)/$_.b))}$p($_.codewords,9,$g($_.codewords,9)*2);if(($_.fcs&1024)!=0){$p($_.codewords,0,$g($_.codewords,0)+659)}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#15856";$k[$j++]=$_.codewords;bwipp_raiseerror()}$_.chars=$a(10);for(var _3f=0;_3f<=9;_3f+=1){$_.i=_3f;var _3i=$g($_.codewords,$_.i);$k[$j++]=_3i;if(_3i<=1286){var _3j=$k[--$j];$k[$j++]=$g($_.onecode_tab513,_3j)}else{var _3l=$k[--$j];$k[$j++]=$g($_.onecode_tab213,_3l-1287)}$k[$j++]=$_.chars;$k[$j++]=$_.i;var _3p=$k[$j-3];var _3q=$k[$j-1];var _3r=$k[$j-2];$j-=3;$p(_3r,_3q,_3p)}for(var _3s=9;_3s>=0;_3s-=1){$_.i=_3s;if(($pow(2,$_.i)&$_.fcs)!=0){$p($_.chars,$_.i,$g($_.chars,$_.i)^8191)}}if($_.height==-1){$_.height=.15}$_.bbs=$a(65);$_.bhs=$a(65);for(var _43=0;_43<=64;_43+=1){$_.i=_43;$_.dec=($g($_.chars,$g($_.onecode_barmap,$_.i*4))&$pow(2,$g($_.onecode_barmap,$_.i*4+1)))!=0;$_.asc=($g($_.chars,$g($_.onecode_barmap,$_.i*4+2))&$pow(2,$g($_.onecode_barmap,$_.i*4+3)))!=0;if(!$_.dec&&!$_.asc){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,2*$_.height/8)}if(!$_.dec&&$_.asc){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($_.dec&&!$_.asc){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($_.dec&&$_.asc){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,8*$_.height/8)}}$k[$j++]=Infinity;var _4m=$_.bbs;var _4n=$_.bhs;$k[$j++]=Infinity;for(var _4p=0,_4q=$_.bhs.length-1;_4p<_4q;_4p++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _4r=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_4m;$k[$j++]="bhs";$k[$j++]=_4n;$k[$j++]="sbs";$k[$j++]=_4r;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="left";$k[$j++]="text1font";$k[$j++]="OCR-B";$k[$j++]="text1yoffset";$k[$j++]=-1;$k[$j++]="text1xoffset";$k[$j++]=-.3;$k[$j++]="opt";$k[$j++]=$_.options;var _4u=$d();$k[$j++]=_4u;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_postnet(){if(!bwipp_postnet.globals){var $__=$_;$_=bwipp_postnet.globals={};$_.postnet_barchars="0123456789";$_.postnet_encs=$a(["55222","22255","22525","22552","25225","25252","25522","52225","52252","52522","5","5"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_postnet.globals);$_.dontdraw=false;$_.includetext=false;$_.validatecheck=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));var _8=$_.barcode.length;if($_.validatecheck){_8=_8-1}$_.barlen=_8;if($_.barlen!=5&&$_.barlen!=9&&$_.barlen!=11){$k[$j++]="bwipp.postnetBadLength#16011";$k[$j++]="USPS POSTNET must be 5, 9 or 11 digits excluding check digit";bwipp_raiseerror()}$F($_.barcode,function(){var _E=$k[--$j];if(_E<48||_E>57){$k[$j++]="bwipp.postnetBadCharacter#16015";$k[$j++]="USPS POSTNET must contain only digits";bwipp_raiseerror()}});$_.checksum=0;for(var _H=0,_G=$_.barlen-1;_H<=_G;_H+=1){$_.i=_H;$_.checksum=$f($_.checksum+($g($_.barcode,$_.i)-48))}$_.checksum=(10-$_.checksum%10)%10;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.postnet_barchars,$_.checksum)){$k[$j++]="bwipp.postnetBadCheckDigit#16033";$k[$j++]="Incorrect USPS POSTNET check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}if($_.height==-1){$_.height=.125}$_.bhs=$a($_.barlen*5+7);$_.enc=$g($_.postnet_encs,10);$_.heights=$a($_.enc.length);for(var _e=0,_d=$_.enc.length-1;_e<=_d;_e+=1){$_.j=_e;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,0,$_.heights);for(var _p=0,_o=$_.barlen-1;_p<=_o;_p+=1){$_.i=_p;$_.enc=$g($_.postnet_encs,$g($_.barcode,$_.i)-48);$_.heights=$a($_.enc.length);for(var _y=0,_x=$_.enc.length-1;_y<=_x;_y+=1){$_.j=_y;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.i*5+1,$_.heights)}$_.enc=$g($_.postnet_encs,$_.checksum);$_.heights=$a($_.enc.length);for(var _1E=0,_1D=$_.enc.length-1;_1E<=_1D;_1E+=1){$_.j=_1E;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.barlen*5+1,$_.heights);$k[$j++]="text";if($_.includecheckintext){var _1Q=$s($_.barlen+1);$P(_1Q,0,$_.barcode);$P(_1Q,$_.barlen,$G($_.postnet_barchars,$_.checksum,1));$k[$j++]=_1Q}else{$k[$j++]=$_.barcode}var _1W=$k[--$j];var _1X=$k[--$j];$_[_1X]=_1W;$_.enc=$g($_.postnet_encs,11);$_.heights=$a($_.enc.length);for(var _1d=0,_1c=$_.enc.length-1;_1d<=_1c;_1d+=1){$_.j=_1d;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.barlen*5+6,$_.heights);$k[$j++]=Infinity;var _1n=$_.bhs;var _1r=[];for(var _1p=0,_1q=$_.bhs.length;_1p<_1q;_1p++){_1r[_1p]=0}_1r=$a(_1r);$k[$j++]=Infinity;for(var _1t=0,_1u=$_.bhs.length-1;_1t<_1u;_1t++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _1v=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bhs";$k[$j++]=_1n;$k[$j++]="bbs";$k[$j++]=_1r;$k[$j++]="sbs";$k[$j++]=_1v;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _23=$d();$k[$j++]=_23;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_planet(){if(!bwipp_planet.globals){var $__=$_;$_=bwipp_planet.globals={};$_.planet_barchars="0123456789";$_.planet_encs=$a(["22555","55522","55252","55225","52552","52525","52255","25552","25525","25255","5","5"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_planet.globals);$_.dontdraw=false;$_.includetext=false;$_.validatecheck=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));var _8=$_.barcode.length;if($_.validatecheck){_8=_8-1}$_.barlen=_8;if($_.barlen!=11&&$_.barlen!=13){$k[$j++]="bwipp.planetBadLength#16193";$k[$j++]="USPS PLANET must be 11 or 13 digits excluding check digit";bwipp_raiseerror()}$F($_.barcode,function(){var _D=$k[--$j];if(_D<48||_D>57){$k[$j++]="bwipp.planetBadCharacter#16197";$k[$j++]="USPS PLANET must contain only digits";bwipp_raiseerror()}});$_.checksum=0;for(var _G=0,_F=$_.barlen-1;_G<=_F;_G+=1){$_.i=_G;$_.checksum=$f($_.checksum+($g($_.barcode,$_.i)-48))}$_.checksum=(10-$_.checksum%10)%10;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.planet_barchars,$_.checksum)){$k[$j++]="bwipp.planetBadCheckDigit#16218";$k[$j++]="Incorrect USPS PLANET check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}if($_.height==-1){$_.height=.125}$_.bhs=$a($_.barlen*5+7);$_.enc=$g($_.planet_encs,10);$_.heights=$a($_.enc.length);for(var _d=0,_c=$_.enc.length-1;_d<=_c;_d+=1){$_.j=_d;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,0,$_.heights);for(var _o=0,_n=$_.barlen-1;_o<=_n;_o+=1){$_.i=_o;$_.enc=$g($_.planet_encs,$g($_.barcode,$_.i)-48);$_.heights=$a($_.enc.length);for(var _x=0,_w=$_.enc.length-1;_x<=_w;_x+=1){$_.j=_x;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.i*5+1,$_.heights)}$_.enc=$g($_.planet_encs,$_.checksum);$_.heights=$a($_.enc.length);for(var _1D=0,_1C=$_.enc.length-1;_1D<=_1C;_1D+=1){$_.j=_1D;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.barlen*5+1,$_.heights);$k[$j++]="text";if($_.includecheckintext){var _1P=$s($_.barlen+1);$P(_1P,0,$_.barcode);$P(_1P,$_.barlen,$G($_.planet_barchars,$_.checksum,1));$k[$j++]=_1P}else{$k[$j++]=$_.barcode}var _1V=$k[--$j];var _1W=$k[--$j];$_[_1W]=_1V;$_.enc=$g($_.planet_encs,11);$_.heights=$a($_.enc.length);for(var _1c=0,_1b=$_.enc.length-1;_1c<=_1b;_1c+=1){$_.j=_1c;$p($_.heights,$_.j,$i($G($_.enc,$_.j,1))*$_.height/5)}$P($_.bhs,$_.barlen*5+6,$_.heights);$k[$j++]=Infinity;var _1m=$_.bhs;var _1q=[];for(var _1o=0,_1p=$_.bhs.length;_1o<_1p;_1o++){_1q[_1o]=0}_1q=$a(_1q);$k[$j++]=Infinity;for(var _1s=0,_1t=$_.bhs.length-1;_1s<_1t;_1s++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _1u=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bhs";$k[$j++]=_1m;$k[$j++]="bbs";$k[$j++]=_1q;$k[$j++]="sbs";$k[$j++]=_1u;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _22=$d();$k[$j++]=_22;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_royalmail(){if(!bwipp_royalmail.globals){var $__=$_;$_=bwipp_royalmail.globals={};$_.royalmail_barchars="ZUVWXY501234B6789AHCDEFGNIJKLMTOPQRS";$_.royalmail_encs=$a(["3300","2211","2301","2310","3201","3210","1122","0033","0123","0132","1023","1032","1302","0213","0303","0312","1203","1212","1320","0231","0321","0330","1221","1230","3102","2013","2103","2112","3003","3012","3120","2031","2121","2130","3021","3030","2","3"]);$_.charvals=new Map;for(var _1=0;_1<=35;_1+=1){$p($_.charvals,$G($_.royalmail_barchars,_1,1),_1)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_royalmail.globals);$_.dontdraw=false;$_.includetext=false;$_.validatecheck=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _5=$k[--$j];$_.options=_5;var _6=$k[--$j];$_.barcode=_6;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.royalmailEmptyData#16406";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.royalmailInputTooLong#16410";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _D=0,_C=$_.barcode.length-1;_D<=_C;_D+=1){if(!$has($_.charvals,$G($_.barcode,_D,1))){$k[$j++]="bwipp.royalmailBadCharacter#16424";$k[$j++]="RM4SCC must contain only capital letters and digits";bwipp_raiseerror()}}var _L=$_.barcode.length;if($_.validatecheck){_L=_L-1}$_.barlen=_L;$_.checksumrow=0;$_.checksumcol=0;for(var _P=0,_O=$_.barlen-1;_P<=_O;_P+=1){$_.i=_P;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$_.checksumrow=$_.checksumrow+~~($_.indx/6);$_.checksumcol=$f($_.checksumcol+$_.indx%6)}$_.checksum=$f($_.checksumrow%6*6+$_.checksumcol%6);if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.royalmail_barchars,$_.checksum)){$k[$j++]="bwipp.royalmailBadCheckDigit#16441";$k[$j++]="Incorrect RM4SCC check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}$_.encstr=$s($_.barlen*4+6);$P($_.encstr,0,$g($_.royalmail_encs,36));for(var _q=0,_p=$_.barlen-1;_q<=_p;_q+=1){$_.i=_q;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.encstr,$_.i*4+1,$g($_.royalmail_encs,$_.indx))}$P($_.encstr,$_.barlen*4+1,$g($_.royalmail_encs,$_.checksum));$k[$j++]="text";if($_.includecheckintext){var _16=$s($_.barlen+1);$P(_16,0,$_.barcode);$P(_16,$_.barlen,$G($_.royalmail_barchars,$_.checksum,1));$k[$j++]=_16}else{$k[$j++]=$_.barcode}var _1C=$k[--$j];var _1D=$k[--$j];$_[_1D]=_1C;$P($_.encstr,$_.barlen*4+5,$g($_.royalmail_encs,37));if($_.height==-1){$_.height=.175}$_.bbs=$a($_.encstr.length);$_.bhs=$a($_.encstr.length);for(var _1O=0,_1N=$_.encstr.length-1;_1O<=_1N;_1O+=1){$_.i=_1O;$_.enc=$G($_.encstr,$_.i,1);if($eq($_.enc,"0")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,2*$_.height/8)}if($eq($_.enc,"1")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"2")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"3")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,8*$_.height/8)}}$k[$j++]=Infinity;var _1u=$_.bbs;var _1v=$_.bhs;$k[$j++]=Infinity;for(var _1x=0,_1y=$_.bhs.length-1;_1x<_1y;_1x++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _1z=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_1u;$k[$j++]="bhs";$k[$j++]=_1v;$k[$j++]="sbs";$k[$j++]=_1z;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _27=$d();$k[$j++]=_27;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_auspost(){if(!bwipp_auspost.globals){var $__=$_;$_=bwipp_auspost.globals={};$_.auspost_encs=$a(["000","001","002","010","011","012","020","021","022","100","101","102","110","111","112","120","121","122","200","201","202","210","211","212","220","221","222","300","301","302","310","311","312","320","321","322","023","030","031","032","033","103","113","123","130","131","132","133","203","213","223","230","231","232","233","303","313","323","330","331","332","333","003","013","00","01","02","10","11","12","20","21","22","30","13","3"]);$_.auspost_barchars="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz #";var _1=new Map([["11",37],["45",37],["59",52],["62",67],["87",37],["92",37]]);$_.auspost_fcclen=_1;$_.auspost_rsprod=function(){var _2=$k[--$j];var _3=$k[--$j];$k[$j++]=$g($_.rstable,$f(_3+_2*64))};$_.rstable=$a(64*64);var _B=$_.rstable;var _A=0;var _9=[];for(var _8=0;_8<64;_8++){_9[_8]=0}_9=$a(_9);$P(_B,_A,_9);var _G=$_.rstable;var _F=64;var _E=[];for(var _D=0;_D<=63;_D+=1){_E[_D]=_D}_E=$a(_E);$P(_G,_F,_E);$k[$j++]=1;for(var _H=0;_H<64;_H++){var _I=$k[$j-1];var _J=_I*2;$k[$j++]=_J;if((_J&64)!=0){var _K=$k[--$j];$k[$j++]=_K^67}var _L=$k[$j-1];var _M=$k[$j-2];$k[$j++]=_M*64;$k[$j++]=_L*64;for(var _N=0;_N<=63;_N+=1){var _P=$k[$j-1];var _R=$k[$j-2];var _T=$g($_.rstable,$f(_R+_N))*2;$k[$j++]=_N;var _X=$_.rstable;var _W=$f(_P+_N);if((_T&64)!=0){_T=_T^67}$p(_X,_W,_T);$j--}$j-=2;var _Y=$k[--$j];$k[$j-1]=_Y}$j--;$_.rstable=$_.rstable;$_.rspoly=$a([1,0,0,0,0]);for(var _b=1;_b<=4;_b+=1){$k[$j++]=$bs(1,_b);for(var _c=_b;_c>=1;_c-=1){var _d=$_.rspoly;var _h=$k[$j-1];$k[$j++]=_d;$k[$j++]=_c;$k[$j++]=$g(_d,_c-1);$k[$j++]=$g($_.rspoly,_c);$k[$j++]=_h;$_.auspost_rsprod();var _i=$k[--$j];var _j=$k[--$j];var _k=$k[--$j];var _l=$k[--$j];$p(_l,_k,$xo(_j,_i))}var _m=$_.rspoly;var _o=$k[$j-1];$k[$j++]=_m;$k[$j++]=0;$k[$j++]=$g(_m,0);$k[$j++]=_o;$_.auspost_rsprod();var _p=$k[--$j];var _q=$k[--$j];var _r=$k[--$j];$p(_r,_q,_p);$j--}$_.rspoly=$_.rspoly;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_auspost.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.custinfoenc="character";$_.modunit=1;bwipp_processoptions();var _t=$k[--$j];$_.options=_t;var _u=$k[--$j];$_.barcode=_u;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($ne($_.custinfoenc,"character")&&$ne($_.custinfoenc,"numeric")){$k[$j++]="bwipp.auspostBadCustinfoEncoding#16675";$k[$j++]="Customer information encoding must be either character or numeric";bwipp_raiseerror()}$_.barlen=$_.barcode.length;if($_.barlen<10){$k[$j++]="bwipp.auspostTooShort#16680";$k[$j++]="The data length must be at least 10";bwipp_raiseerror()}$k[$j++]=$_.auspost_fcclen;if($_.barlen>=2){$k[$j++]=$G($_.barcode,0,2)}else{$k[$j++]=""}var _14=$k[$j-1];var _15=$k[$j-2];var _16=$has(_15,_14);if(!_16){$j-=2;$k[$j++]="bwipp.auspostBadFCC#16686";$k[$j++]="Must begin with an FCC, either 11, 45, 59, 62, 87 or 92";bwipp_raiseerror()}var _17=$k[--$j];var _18=$k[--$j];$_.encstr=$s($g(_18,_17));$k[$j++]=true;for(var _1B=0;_1B<1;_1B++){if($_.barlen<10){$k[$j-1]=false;break}var _1E=$G($_.barcode,2,8);for(var _1F=0,_1G=_1E.length;_1F<_1G;_1F++){var _1H=$g(_1E,_1F);if(_1H<48||_1H>57){$k[$j-1]=false;break}}}var _1I=$k[--$j];if(!_1I){$k[$j++]="bwipp.auspostIncompleteDPID#16699";$k[$j++]="The DPID must be 8 digits";bwipp_raiseerror()}var _1L=$eq($_.custinfoenc,"numeric")?2:3;if(($_.barlen-10)*_1L>$_.encstr.length-22-14){$k[$j++]="bwipp.auspostTooLong#16707";$k[$j++]="The message is too long";bwipp_raiseerror()}$P($_.encstr,0,$g($_.auspost_encs,74));for(var _1P=0;_1P<=1;_1P+=1){$_.i=_1P;$P($_.encstr,$_.i*2+2,$g($_.auspost_encs,$i($G($_.barcode,$_.i,1))+64))}for(var _1W=2;_1W<=9;_1W+=1){$_.i=_1W;$P($_.encstr,$_.i*2+2,$g($_.auspost_encs,$i($G($_.barcode,$_.i,1))+64))}if($eq($_.custinfoenc,"numeric")){for(var _1g=0,_1f=$_.barlen-11;_1g<=_1f;_1g+=1){$_.i=_1g;var _1j=$g($_.barcode,$_.i+10);$k[$j++]=$_.auspost_encs;$k[$j++]=_1j;if(_1j<48||_1j>57){$j-=2;$k[$j++]="bwipp.auspostInvalidCustinfoDigit#16747";$k[$j++]="The customer information data contains a non-digit character";bwipp_raiseerror()}var _1k=$k[--$j];var _1l=$k[--$j];$P($_.encstr,$_.i*2+22,$g(_1l,_1k-48+64))}$_.ciflen=($_.barlen-10)*2}else{for(var _1s=0,_1r=$_.barlen-11;_1s<=_1r;_1s+=1){$_.i=_1s;$X($_.auspost_barchars,$G($_.barcode,$_.i+10,1));var _1w=$k[--$j];if(!_1w){$k[$j-1]="bwipp.auspostInvalidCustinfoCharacter#16758";$k[$j++]="The customer information contains an invalid character";bwipp_raiseerror()}var _1x=$k[--$j];$_.indx=_1x.length;$j-=2;$_.enc=$g($_.auspost_encs,$_.indx);$P($_.encstr,$_.i*3+22,$_.enc)}$_.ciflen=($_.barlen-10)*3}for(var _27=22+$_.ciflen,_26=$_.encstr.length-14;_27<=_26;_27+=1){$P($_.encstr,_27,$g($_.auspost_encs,75))}$_.rscodes=$a(~~(($_.encstr.length-16)/3)+4);var _2G=$_.rscodes;var _2F=0;var _2E=[];for(var _2D=0;_2D<4;_2D++){_2E[_2D]=0}_2E=$a(_2E);$P(_2G,_2F,_2E);for(var _2J=2,_2I=$_.encstr.length-16;_2J<=_2I;_2J+=3){$_.i=_2J;$p($_.rscodes,$_.rscodes.length-~~(($_.i-2)/3)-1,$i($G($_.encstr,$_.i,1))*16+$i($G($_.encstr,$_.i+1,1))*4+$i($G($_.encstr,$_.i+2,1)))}for(var _2X=$_.rscodes.length-5;_2X>=0;_2X-=1){$k[$j++]=_2X;for(var _2Y=0;_2Y<=4;_2Y+=1){var _2Z=$_.rscodes;var _2a=$k[$j-1];var _2b=$g(_2Z,$f(_2a+_2Y));$k[$j++]=_2Z;$k[$j++]=$f(_2a+_2Y);$k[$j++]=_2b;$k[$j++]=$g($_.rspoly,_2Y);$k[$j++]=$g($_.rscodes,_2a+4);$_.auspost_rsprod();var _2g=$k[--$j];var _2h=$k[--$j];var _2i=$k[--$j];var _2j=$k[--$j];$p(_2j,_2i,$xo(_2h,_2g))}$j--}$_.checkcode=$Z($s(12),"000000000000");for(var _2m=0;_2m<=3;_2m+=1){$_.i=_2m;$_.enc=$R($s(3),$g($_.rscodes,3-$_.i),4);$P($_.checkcode,$_.i*3+(3-$_.enc.length),$_.enc)}$P($_.encstr,$_.encstr.length-14,$_.checkcode);$P($_.encstr,$_.encstr.length-2,$g($_.auspost_encs,74));if($_.height==-1){$_.height=.175}$_.bbs=$a($_.encstr.length);$_.bhs=$a($_.encstr.length);for(var _39=0,_38=$_.encstr.length-1;_39<=_38;_39+=1){$_.i=_39;$_.enc=$G($_.encstr,$_.i,1);if($eq($_.enc,"0")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,8*$_.height/8)}if($eq($_.enc,"1")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"2")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"3")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,2*$_.height/8)}}$k[$j++]=Infinity;var _3f=$_.bbs;var _3g=$_.bhs;$k[$j++]=Infinity;for(var _3i=0,_3j=$_.bhs.length-1;_3i<_3j;_3i++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _3k=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_3f;$k[$j++]="bhs";$k[$j++]=_3g;$k[$j++]="sbs";$k[$j++]=_3k;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$G($_.barcode,2,$_.barlen-2),0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _3u=$d();$k[$j++]=_3u;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_kix(){if(!bwipp_kix.globals){var $__=$_;$_=bwipp_kix.globals={};$_.kix_encs=$a(["0033","0123","0132","1023","1032","1122","0213","0303","0312","1203","1212","1302","0231","0321","0330","1221","1230","1320","2013","2103","2112","3003","3012","3102","2031","2121","2130","3021","3030","3120","2211","2301","2310","3201","3210","3300"]);$_.kix_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";$_.charvals=new Map;for(var _1=0;_1<=35;_1+=1){$p($_.charvals,$G($_.kix_barchars,_1,1),_1)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_kix.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _5=$k[--$j];$_.options=_5;var _6=$k[--$j];$_.barcode=_6;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.kixEmptyData#16967";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.kixInputTooLong#16971";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _D=0,_C=$_.barcode.length-1;_D<=_C;_D+=1){if(!$has($_.charvals,$G($_.barcode,_D,1))){$k[$j++]="bwipp.kixBadCharacter#16979";$k[$j++]="KIX must contain only capital letters and digits";bwipp_raiseerror()}}$_.barlen=$_.barcode.length;$_.encstr=$s($_.barlen*4);for(var _N=0,_M=$_.barlen-1;_N<=_M;_N+=1){$_.i=_N;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.encstr,$_.i*4,$g($_.kix_encs,$_.indx))}if($_.height==-1){$_.height=.175}$_.bbs=$a($_.encstr.length);$_.bhs=$a($_.encstr.length);for(var _e=0,_d=$_.encstr.length-1;_e<=_d;_e+=1){$_.i=_e;$_.enc=$G($_.encstr,$_.i,1);if($eq($_.enc,"0")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,2*$_.height/8)}if($eq($_.enc,"1")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"2")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"3")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,8*$_.height/8)}}$k[$j++]=Infinity;var _1A=$_.bbs;var _1B=$_.bhs;$k[$j++]=Infinity;for(var _1D=0,_1E=$_.bhs.length-1;_1D<_1E;_1D++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _1F=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_1A;$k[$j++]="bhs";$k[$j++]=_1B;$k[$j++]="sbs";$k[$j++]=_1F;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.barcode,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _1N=$d();$k[$j++]=_1N;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_japanpost(){if(!bwipp_japanpost.globals){var $__=$_;$_=bwipp_japanpost.globals={};$_.japanpost_encs=$a(["300","330","312","132","321","303","123","231","213","033","030","120","102","210","012","201","021","003","333","31","13"]);$_.japanpost_barchars="0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_japanpost.globals);$_.dontdraw=false;$_.includetext=false;$_.includecheckintext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.japanpostEmptyData#17121";$k[$j++]="The data must not be empty";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(!(_7>=48&&_7<=57||(_7>=65&&_7<=90||_7==45))){$k[$j++]="bwipp.japanpostBadCharacter#17131";$k[$j++]="Japan Post must contain only digits, capital letters and the dash symbol";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;$_.encstr=$s((20+1)*3+4);$_.digits=$a(20);$P($_.encstr,0,$g($_.japanpost_encs,19));$_.checksum=0;$_.j=0;$_.i=0;$k[$j++]=true;for(var _F=0,_E=$_.barlen-1;_F<=_E;_F+=1){$_.i=_F;$X($_.japanpost_barchars,$G($_.barcode,$_.i,1));$j--;var _J=$k[--$j];$_.indx=_J.length;$j-=2;if($_.indx>=11&&$_.indx<37){if($_.j>18){$k[$j-1]=false;break}$_.digit=~~(($_.indx-1)/10)+10;$P($_.encstr,$_.j*3+2,$g($_.japanpost_encs,$_.digit));$_.checksum=$_.checksum+$_.digit;$p($_.digits,$_.j,$_.digit);$_.j=$_.j+1}if($_.j>19){$k[$j-1]=false;break}$_.digit=0;if($_.indx==37){$_.digit=14}if($_.indx>=11&&$_.indx<37){$_.digit=($_.indx-1)%10}if($_.indx<11){$_.digit=$_.indx}$P($_.encstr,$_.j*3+2,$g($_.japanpost_encs,$_.digit));$_.checksum=$_.checksum+$_.digit;$p($_.digits,$_.j,$_.digit);$_.j=$_.j+1}var _p=$k[--$j];if(!_p){$k[$j++]="bwipp.japanpostTooLong#17189";$k[$j++]="The input is too long";bwipp_raiseerror()}for(var _s=$_.j,_r=20-1;_s<=_r;_s+=1){$_.k=_s;$P($_.encstr,$_.k*3+2,$g($_.japanpost_encs,14));$_.checksum=$_.checksum+14;$p($_.digits,$_.k,14)}$_.checksum=19-$_.checksum%19;$P($_.encstr,2+20*3,$g($_.japanpost_encs,$_.checksum));$k[$j++]="text";if($_.includecheckintext){var _15=$s($_.barlen+1);$P(_15,0,$_.barcode);$P(_15,$_.barlen,$G($_.japanpost_barchars,$_.checksum,1));$k[$j++]=_15}else{$k[$j++]=$_.barcode}var _1B=$k[--$j];var _1C=$k[--$j];$_[_1C]=_1B;$P($_.encstr,(20+1)*3+2,$g($_.japanpost_encs,20));if($_.height==-1){$_.height=.175}$_.bbs=$a(21*3+4);$_.bhs=$a($_.bbs.length);for(var _1L=0,_1K=$_.bbs.length-1;_1L<=_1K;_1L+=1){$_.i=_1L;$_.enc=$G($_.encstr,$_.i,1);$_.bunit=0;$_.hunit=0;if($eq($_.enc,"0")){$_.bunit=3;$_.hunit=2}if($eq($_.enc,"1")){$_.bunit=0;$_.hunit=5}if($eq($_.enc,"2")){$_.bunit=3;$_.hunit=5}if($eq($_.enc,"3")){$_.bunit=0;$_.hunit=8}$p($_.bbs,$_.i,$_.bunit*$_.height/8);$p($_.bhs,$_.i,$_.hunit*$_.height/8)}$k[$j++]=Infinity;var _1b=$_.bbs;var _1c=$_.bhs;$k[$j++]=Infinity;for(var _1e=0,_1f=$_.bhs.length-1;_1e<_1f;_1e++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _1g=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_1b;$k[$j++]="bhs";$k[$j++]=_1c;$k[$j++]="sbs";$k[$j++]=_1g;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _1o=$d();$k[$j++]=_1o;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_msi(){if(!bwipp_msi.globals){var $__=$_;$_=bwipp_msi.globals={};$_.msi_encs=$a(["12121212","12121221","12122112","12122121","12211212","12211221","12212112","12212121","21121212","21121221","21","121"]);$_.msi_barchars="0123456789";var _1=new Map([["mod10",function(){$_.mod10()}],["mod1010",function(){$_.mod10();$_.mod10()}],["mod11",function(){$_.mod11()}],["ncrmod11",function(){$_.ncrmod11()}],["mod1110",function(){$_.mod11();$_.mod10()}],["ncrmod1110",function(){$_.ncrmod11();$_.mod10()}]]);$_.msi_checkfunc=_1;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_msi.globals);$_.dontdraw=false;$_.includecheck=false;$_.includetext=false;$_.includecheckintext=false;$_.checktype="unset";$_.badmod11=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;if($_.barcode.length>500){$k[$j++]="bwipp.msiinputTooLong#17353";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($ne($_.checktype,"unset")&&!$_.includecheck){$k[$j++]="bwipp.msiCheckTypeWithoutCheck#17362";$k[$j++]="checktype requires includecheck";bwipp_raiseerror()}if($_.badmod11&&($ne($_.checktype,"mod11")&&$ne($_.checktype,"ncrmod11")&&$ne($_.checktype,"mod1110")&&$ne($_.checktype,"ncrmod1110"))){$k[$j++]="bwipp.msiBadMod11Mismatch#17366";$k[$j++]="badmod11 requires checktype with mod11";bwipp_raiseerror()}if($eq($_.checktype,"unset")){$_.checktype="mod10"}$F($_.barcode,function(){var _G=$k[--$j];if(_G<48||_G>57){$k[$j++]="bwipp.msiBadCharacter#17376";$k[$j++]="MSI must contain only digits";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;$_.txtlen=$_.barlen;$_.mod10=function(){var _J=$k[--$j];$_.code=_J;$k[$j++]=0;$k[$j++]=0;$F($_.code,function(){var _L=$k[--$j];var _M=$k[--$j];var _N=$k[--$j];$k[$j++]=$f(_L-48+_M*10);$k[$j++]=_N});$j--;var _O=$k[--$j];$k[$j++]=_O*2;$k[$j++]=0;for(;;){var _P=$k[--$j];var _Q=$k[--$j];var _R=~~(_Q/10);$k[$j++]=$f(_P+_Q%10);$k[$j++]=_R;if(_R==0){$j--;break}var _S=$k[--$j];var _T=$k[--$j];$k[$j++]=_S;$k[$j++]=_T}$k[$j++]=0;$k[$j++]=0;$F($_.code,function(){var _V=$k[--$j];var _W=$k[--$j];var _X=$k[--$j];$k[$j++]=$f(_W+(_V-48));$k[$j++]=_X});var _Y=$k[--$j];$j--;var _b=$k[--$j];var _d=$s($_.code.length+1);$P(_d,0,$_.code);$p(_d,$_.code.length,(10-$f(_b+_Y)%10)%10+48);$k[$j++]=_d};$_.mod11=function(){var _g=$k[--$j];$_.code=_g;$k[$j++]=$_.code.length-1;$F($_.code,function(){var _j=$k[--$j];var _k=$k[--$j];$k[$j++]=_j-48;$k[$j++]=_k%6+2;$k[$j++]=_k-1});$k[$j-1]=0;for(var _m=0,_n=$_.code.length;_m<_n;_m++){var _o=$k[$j-2];var _p=$k[$j-3];var _q=$k[$j-1];$j-=3;$k[$j++]=$f(_q+_p*_o)}var _r=$k[--$j];var _s=(11-_r%11)%11;$k[$j++]=_s;if(_s==10){if($_.badmod11){$j--;var _v=$s($_.code.length+2);$P(_v,0,$_.code);$P(_v,$_.code.length,"10");$k[$j++]=_v}else{$k[$j-1]="bwipp.msiBadMod11NotSpecified#17407";$k[$j++]="mod11 check digit is 10 but badmod11 not specified";bwipp_raiseerror()}}else{var _z=$s($_.code.length+1);$P(_z,0,$_.code);$k[$j++]=_z;$k[$j++]=_z;$k[$j++]=$_.code.length;$r(4,-1);var _12=$k[--$j];var _13=$k[--$j];var _14=$k[--$j];$p(_14,_13,_12+48)}};$_.ncrmod11=function(){var _15=$k[--$j];$_.code=_15;$k[$j++]=$_.code.length-1;$F($_.code,function(){var _18=$k[--$j];var _19=$k[--$j];$k[$j++]=_18-48;$k[$j++]=_19%8+2;$k[$j++]=_19-1});$k[$j-1]=0;for(var _1B=0,_1C=$_.code.length;_1B<_1C;_1B++){var _1D=$k[$j-2];var _1E=$k[$j-3];var _1F=$k[$j-1];$j-=3;$k[$j++]=$f(_1F+_1E*_1D)}var _1G=$k[--$j];var _1H=(11-_1G%11)%11;$k[$j++]=_1H;if(_1H==10){if($_.badmod11){$j--;var _1K=$s($_.code.length+2);$P(_1K,0,$_.code);$P(_1K,$_.code.length,"10");$k[$j++]=_1K}else{$k[$j-1]="bwipp.msiBadMod11NotSpecified#17423";$k[$j++]="mod11 check digit is 10 but badmod11 not specified";bwipp_raiseerror()}}else{var _1O=$s($_.code.length+1);$P(_1O,0,$_.code);$k[$j++]=_1O;$k[$j++]=_1O;$k[$j++]=$_.code.length;$r(4,-1);var _1R=$k[--$j];var _1S=$k[--$j];var _1T=$k[--$j];$p(_1T,_1S,_1R+48)}};if($_.includecheck){var _1V=$_.checktype;var _1W=$has($_.msi_checkfunc,_1V);$k[$j++]=$_.msi_checkfunc;$k[$j++]=_1V;if(!_1W){$j-=2;$k[$j++]="bwipp.msiBadCharacter#17435";$k[$j++]="MSI checktype must be mod10, mod1010, mod11, ncrmod11, mod1110 or ncrmod1110";bwipp_raiseerror()}var _1X=$k[--$j];var _1Y=$k[--$j];$k[$j++]=$_.barcode;if($g(_1Y,_1X)()===true){return true}var _1b=$k[--$j];$_.barcode=_1b;$_.barlen=$_.barcode.length;if($_.includecheckintext){$_.txtlen=$_.barlen}}$_.sbs=$s($_.barlen*8+5);$P($_.sbs,0,$g($_.msi_encs,10));for(var _1l=0,_1k=$_.barlen-1;_1l<=_1k;_1l+=1){$_.i=_1l;$X($_.msi_barchars,$G($_.barcode,$_.i,1));$j--;var _1p=$k[--$j];$_.indx=_1p.length;$j-=2;$_.enc=$g($_.msi_encs,$_.indx);$P($_.sbs,$_.i*8+2,$_.enc)}$P($_.sbs,$_.barlen*8+2,$g($_.msi_encs,11));if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _23=[];var _1z=$_.sbs;for(var _20=0,_21=_1z.length;_20<_21;_20++){_23[_20]=$g(_1z,_20)-48}_23=$a(_23);var _28=[];for(var _25=0,_26=~~(($_.sbs.length+1)/2);_25<_26;_25++){_28[_25]=$_.height}_28=$a(_28);var _2C=[];for(var _2A=0,_2B=~~(($_.sbs.length+1)/2);_2A<_2B;_2A++){_2C[_2A]=0}_2C=$a(_2C);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_23;$k[$j++]="bhs";$k[$j++]=_28;$k[$j++]="bbs";$k[$j++]=_2C;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$G($_.barcode,0,$_.txtlen),0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2M=$d();$k[$j++]=_2M;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_plessey(){if(!bwipp_plessey.globals){var $__=$_;$_=bwipp_plessey.globals={};$_.plessey_barchars="0123456789ABCDEF";$_.plessey_encs=$a(["14141414","32141414","14321414","32321414","14143214","32143214","14323214","32323214","14141432","32141432","14321432","32321432","14143232","32143232","14323232","32323232","32321432","541412323","323"]);$_.plessey_checksalt=$a([1,1,1,1,0,1,0,0,1]);$_.plessey_checkinit=$a([0,0,0,0,0,0,0,0]);$_.charvals=new Map;for(var _3=0;_3<=15;_3+=1){$p($_.charvals,$G($_.plessey_barchars,_3,1),_3)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_plessey.globals);$_.dontdraw=false;$_.includetext=false;$_.validatecheck=false;$_.includecheckintext=false;$_.unidirectional=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _7=$k[--$j];$_.options=_7;var _8=$k[--$j];$_.barcode=_8;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.plesseyEmptyData#17600";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.plesseyInputTooLong#17604";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _F=0,_E=$_.barcode.length-1;_F<=_E;_F+=1){if(!$has($_.charvals,$G($_.barcode,_F,1))){$k[$j++]="bwipp.plesseyBadCharacter#17612";$k[$j++]="Plessey must contain only digits and letters A B C D E F";bwipp_raiseerror()}}if($_.validatecheck&&$_.barcode.length<2){$k[$j++]="bwipp.plesseyBadLength#17617";$k[$j++]="Plessey with check digits must be at least 2 characters";bwipp_raiseerror()}var _P=$_.barcode.length;if($_.validatecheck){_P=_P-2}$_.barlen=_P;$_.checkbits=$a($_.barlen*4+8);$P($_.checkbits,$_.barlen*4,$_.plessey_checkinit);for(var _X=0,_W=$_.barlen-1;_X<=_W;_X+=1){$_.i=_X;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.checkbits,$_.i*4,$a([$_.indx&1,$flr($_.indx/2)&1,$flr($_.indx/4)&1,$flr($_.indx/8)]))}for(var _m=0,_l=$_.barlen*4-1;_m<=_l;_m+=1){$_.i=_m;if($g($_.checkbits,$_.i)==1){for(var _q=0;_q<=8;_q+=1){$_.j=_q;$p($_.checkbits,$_.i+$_.j,$xo($g($_.checkbits,$_.i+$_.j),$g($_.plessey_checksalt,$_.j)))}}}$_.checkval=0;for(var _10=0;_10<=7;_10+=1){$_.i=_10;$_.checkval=$f($_.checkval+$pow(2,$_.i)*$g($_.checkbits,$f($_.barlen*4+$_.i)))}$_.checksum1=$_.checkval&15;$_.checksum2=$flr($_.checkval/16);if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.plessey_barchars,$_.checksum1)||$g($_.barcode,$_.barlen+1)!=$g($_.plessey_barchars,$_.checksum2)){$k[$j++]="bwipp.plesseyBadCheckDigits#17662";$k[$j++]="Incorrect Plessey check digits provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _1P=$_.unidirectional?27:33;$_.sbs=$s($f($_.barlen*8+_1P));$P($_.sbs,0,$g($_.plessey_encs,16));for(var _1V=0,_1U=$_.barlen-1;_1V<=_1U;_1V+=1){$_.i=_1V;$_.indx=$g($_.charvals,$G($_.barcode,$_.i,1));$P($_.sbs,$_.i*8+8,$g($_.plessey_encs,$_.indx))}$P($_.sbs,$_.barlen*8+8,$g($_.plessey_encs,$_.checksum1));$P($_.sbs,$_.barlen*8+16,$g($_.plessey_encs,$_.checksum2));$k[$j++]="text";if($_.includecheckintext){var _1p=$s($_.barlen+2);$P(_1p,0,$_.barcode);$P(_1p,$_.barlen,$G($_.plessey_barchars,$_.checksum1,1));$P(_1p,$_.barlen+1,$G($_.plessey_barchars,$_.checksum2,1));$k[$j++]=_1p}else{$k[$j++]=$_.barcode}var _1y=$k[--$j];var _1z=$k[--$j];$_[_1z]=_1y;var _23=$_.unidirectional?18:17;$P($_.sbs,$_.barlen*8+24,$g($_.plessey_encs,_23));if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _2A=[];var _26=$_.sbs;for(var _27=0,_28=_26.length;_27<_28;_27++){_2A[_27]=$g(_26,_27)-48}_2A=$a(_2A);var _2F=[];for(var _2C=0,_2D=~~(($_.sbs.length+1)/2);_2C<_2D;_2C++){_2F[_2C]=$_.height}_2F=$a(_2F);var _2J=[];for(var _2H=0,_2I=~~(($_.sbs.length+1)/2);_2H<_2I;_2H++){_2J[_2H]=0}_2J=$a(_2J);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_2A;$k[$j++]="bhs";$k[$j++]=_2F;$k[$j++]="bbs";$k[$j++]=_2J;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _2R=$d();$k[$j++]=_2R;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_telepen(){if(!bwipp_telepen.globals){var $__=$_;$_=bwipp_telepen.globals={};$_.telepen_encs=$a(["31313131","1131313111","33313111","1111313131","3111313111","11333131","13133131","111111313111","31333111","1131113131","33113131","1111333111","3111113131","1113133111","1311133111","111111113131","3131113111","11313331","333331","111131113111","31113331","1133113111","1313113111","1111113331","31131331","113111113111","3311113111","1111131331","311111113111","1113111331","1311111331","11111111113111","31313311","1131311131","33311131","1111313311","3111311131","11333311","13133311","111111311131","31331131","1131113311","33113311","1111331131","3111113311","1113131131","1311131131","111111113311","3131111131","1131131311","33131311","111131111131","3111131311","1133111131","1313111131","111111131311","3113111311","113111111131","3311111131","111113111311","311111111131","111311111311","131111111311","11111111111131","3131311111","11313133","333133","111131311111","31113133","1133311111","1313311111","1111113133","313333","113111311111","3311311111","11113333","311111311111","11131333","13111333","11111111311111","31311133","1131331111","33331111","1111311133","3111331111","11331133","13131133","111111331111","3113131111","1131111133","33111133","111113131111","3111111133","111311131111","131111131111","111111111133","31311313","113131111111","3331111111","1111311313","311131111111","11331313","13131313","11111131111111","3133111111","1131111313","33111313","111133111111","3111111313","111313111111","131113111111","111111111313","313111111111","1131131113","33131113","11113111111111","3111131113","113311111111","131311111111","111111131113","3113111113","11311111111111","331111111111","111113111113","31111111111111","111311111113","131111111113","1111111111111111"]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_telepen.globals);$_.numeric=false;$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.parse=false;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.telepeninputTooLong#17819";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _9=$k[--$j];$_.barcode=_9;$_.barlen=$_.barcode.length;$del($_.options,"parse");if($_.numeric&&$_.barlen%2!=0){$k[$j++]="bwipp.telepenNumericOddLength#17840";$k[$j++]="Telepen Numeric must have an even length";bwipp_raiseerror()}$_.sbs=$s($_.barlen*16+48);$_.enc=$g($_.telepen_encs,95);$P($_.sbs,0,$_.enc);$_.l=$_.enc.length;$_.checksum=0;$_.i=0;$_.j=0;for(;;){if($_.i==$_.barlen){break}if($_.numeric){if($g($_.barcode,$_.i)>16){$_.np=$G($_.barcode,$_.i,2);var _U=$g($_.np,0);var _W=$g($_.np,1);if(_U<48||_U>57||(_W<48||_W>57)&&!(_W==88)){$k[$j++]=-1}else{var _b=$g($_.np,1)-48;$k[$j++]=$g($_.np,0)-48;$k[$j++]=_b;if(_b!=40){var _c=$k[--$j];var _d=$k[--$j];$k[$j++]=$f(_c+_d*10)+10}else{$j--}$k[$j-1]+=17}var _f=$k[$j-1];if(_f==-1){$k[$j-1]="bwipp.telepenInvalidNumericCharacter#17875";$k[$j++]="Telepen Numeric may contain only digits, or X in even positions";bwipp_raiseerror()}var _g=$k[--$j];$_.indx=_g;$_.i=$_.i+2}else{$_.indx=$g($_.barcode,$_.i);$_.i=$_.i+1}}else{$_.indx=$g($_.barcode,$_.i);if($_.indx>127){$k[$j++]="bwipp.telepenInvalidAlphaCharacter#17887";$k[$j++]="Telepen Alpha characters must have ordinal values 0 to 127";bwipp_raiseerror()}$_.i=$_.i+1}$_.checksum=$f($_.checksum+$_.indx);$_.enc=$g($_.telepen_encs,$_.indx);$P($_.sbs,$_.l,$_.enc);$_.l=$_.l+$_.enc.length;$_.j=$_.j+1}$_.checksum=(127-$_.checksum%127)%127;$_.enc=$g($_.telepen_encs,$_.checksum);$P($_.sbs,$_.l,$_.enc);$_.l=$_.l+$_.enc.length;$_.enc=$g($_.telepen_encs,122);$P($_.sbs,$_.l,$_.enc);$_.l=$_.l+$_.enc.length;$_.sbs=$G($_.sbs,0,$_.l);$_.text=$s($_.barcode.length);for(var _1M=0,_1L=$_.barcode.length-1;_1M<=_1L;_1M+=1){$_.i=_1M;$_.c=$g($_.barcode,$_.i);var _1X=$_.text;var _1W=$_.i;var _1V=$_.c>=32&&$_.c<=126?$_.c:32;$p(_1X,_1W,_1V)}$k[$j++]=Infinity;var _1Y=$_.sbs;for(var _1Z=0,_1a=_1Y.length;_1Z<_1a;_1Z++){$k[$j++]=$g(_1Y,_1Z)-48}$_.sbs=$a();if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _1d=$_.sbs;var _1i=[];for(var _1f=0,_1g=~~(($_.sbs.length+1)/2);_1f<_1g;_1f++){_1i[_1f]=$_.height}_1i=$a(_1i);var _1m=[];for(var _1k=0,_1l=~~(($_.sbs.length+1)/2);_1k<_1l;_1k++){_1m[_1k]=0}_1m=$a(_1m);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1d;$k[$j++]="bhs";$k[$j++]=_1i;$k[$j++]="bbs";$k[$j++]=_1m;if($_.includetext){$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center"}$k[$j++]="opt";$k[$j++]=$_.options;var _1u=$d();$k[$j++]=_1u;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_telepennumeric(){var $__=$_;$_=Object.assign({},$_,bwipp_telepennumeric.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"numeric",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_telepen();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_posicode(){if(!bwipp_posicode.globals){var $__=$_;$_=bwipp_posicode.globals={};$_.posicode_la0=-1;$_.posicode_la1=-2;$_.posicode_la2=-3;$_.posicode_sf0=-4;$_.posicode_sf1=-5;$_.posicode_sf2=-6;$_.posicode_fn1=-7;$_.posicode_fn2=-8;$_.posicode_fn3=-9;$_.posicode_fn4=-10;$_.la0=$_.posicode_la0;$_.la1=$_.posicode_la1;$_.la2=$_.posicode_la2;$_.sf0=$_.posicode_sf0;$_.sf1=$_.posicode_sf1;$_.sf2=$_.posicode_sf2;$_.fn1=$_.posicode_fn1;$_.fn2=$_.posicode_fn2;$_.fn3=$_.posicode_fn3;$_.fn4=$_.posicode_fn4;$_.posicode_charmapsnormal=$a([$a(["0","^","'"]),$a(["1",";",27]),$a(["2","<",28]),$a(["3","=",29]),$a(["4",">",30]),$a(["5","?",31]),$a(["6","@","!"]),$a(["7","[",'"']),$a(["8",92,"#"]),$a(["9","]","&"]),$a(["A","a",1]),$a(["B","b",2]),$a(["C","c",3]),$a(["D","d",4]),$a(["E","e",5]),$a(["F","f",6]),$a(["G","g",7]),$a(["H","h",8]),$a(["I","i",9]),$a(["J","j",10]),$a(["K","k",11]),$a(["L","l",12]),$a(["M","m",13]),$a(["N","n",14]),$a(["O","o",15]),$a(["P","p",16]),$a(["Q","q",17]),$a(["R","r",18]),$a(["S","s",19]),$a(["T","t",20]),$a(["U","u",21]),$a(["V","v",22]),$a(["W","w",23]),$a(["X","x",24]),$a(["Y","y",25]),$a(["Z","z",26]),$a(["-","_",40]),$a([".","`",41]),$a([" ",127,0]),$a(["$","{","*"]),$a(["/","|",","]),$a(["+","}",":"]),$a(["%","~",$_.fn1]),$a([$_.la1,$_.la0,$_.fn2]),$a([$_.sf1,$_.sf0,$_.fn3]),$a([$_.sf2,$_.sf2,$_.fn4])]);$_.posicode_charmapslimited=$a([$a(["0",-98,-98]),$a(["1",-98,-98]),$a(["2",-98,-98]),$a(["3",-98,-98]),$a(["4",-98,-98]),$a(["5",-98,-98]),$a(["6",-98,-98]),$a(["7",-98,-98]),$a(["8",-98,-98]),$a(["9",-98,-98]),$a(["A",-98,-98]),$a(["B",-98,-98]),$a(["C",-98,-98]),$a(["D",-98,-98]),$a(["E",-98,-98]),$a(["F",-98,-98]),$a(["G",-98,-98]),$a(["H",-98,-98]),$a(["I",-98,-98]),$a(["J",-98,-98]),$a(["K",-98,-98]),$a(["L",-98,-98]),$a(["M",-98,-98]),$a(["N",-98,-98]),$a(["O",-98,-98]),$a(["P",-98,-98]),$a(["Q",-98,-98]),$a(["R",-98,-98]),$a(["S",-98,-98]),$a(["T",-98,-98]),$a(["U",-98,-98]),$a(["V",-98,-98]),$a(["W",-98,-98]),$a(["X",-98,-98]),$a(["Y",-98,-98]),$a(["Z",-98,-98]),$a(["-",-98,-98]),$a([".",-98,-98])]);var _1c=new Map([["a",$a(["141112","131212","121312","111412","131113","121213","111313","121114","111214","111115","181111","171211","161311","151411","141511","131611","121711","111811","171112","161212","151312","141412","131512","121612","111712","161113","151213","141313","131413","121513","111613","151114","141214","131314","121414","111514","141115","131215","121315","111415","131116","121216","111316","121117","111217","111118","1<111112","111111111;1"])],["b",$a(["151213","141313","131413","121513","141214","131314","121414","131215","121315","121216","191212","181312","171412","161512","151612","141712","131812","121912","181213","171313","161413","151513","141613","131713","121813","171214","161314","151414","141514","131614","121714","161215","151315","141415","131515","121615","151216","141316","131416","121516","141217","131317","121417","131218","121318","121219","1<121312","121212121<1"])],["limiteda",$a(["111411","111312","111213","111114","121311","121212","121113","141111","131211","131112","171111","161211","151311","141411","131511","121611","111711","161112","151212","141312","131412","121512","111612","151113","141213","131313","121413","111513","141114","131214","121314","111414","131115","121215","111315","121116","111216","111117","151111","1"])],["limitedb",$a(["121512","121413","121314","121215","131412","131313","131214","151212","141312","141213","181212","171312","161412","151512","141612","131712","121812","171213","161313","151413","141513","131613","121713","161214","151314","141414","131514","121614","151215","141315","131415","121515","141216","131316","121416","131217","121317","121218","141212","1"])]]);$_.posicode_encmaps=_1c;$_.posicode_c2w=$a([$a([495,330,210,126,70,35,15,5]),$a([165,120,84,56,35,20,10,4]),$a([45,36,28,21,15,10,6,3]),$a([9,8,7,6,5,4,3,2]),$a([1,1,1,1,1,1,1,1])]);$_.charvals_normal=$a([new Map,new Map,new Map]);for(var _1k=0,_1j=$_.posicode_charmapsnormal.length-1;_1k<=_1j;_1k+=1){$_.i=_1k;$_.encs=$g($_.posicode_charmapsnormal,$_.i);for(var _1n=0;_1n<=2;_1n+=1){$_.j=_1n;var _1q=$g($_.encs,$_.j);var _1r=$t(_1q);if($eq(_1r,"stringtype")){_1q=$g(_1q,0)}$p($g($_.charvals_normal,$_.j),_1q,$_.i)}}$_.charvals_normal=$_.charvals_normal;$_.charvals_limited=$a([new Map,new Map,new Map]);for(var _21=0,_20=$_.posicode_charmapslimited.length-1;_21<=_20;_21+=1){$_.i=_21;$_.encs=$g($_.posicode_charmapslimited,$_.i);for(var _24=0;_24<=2;_24+=1){$_.j=_24;var _27=$g($_.encs,$_.j);var _28=$t(_27);if($eq(_28,"stringtype")){_27=$g(_27,0)}$p($g($_.charvals_limited,$_.j),_27,$_.i)}}$_.charvals_limited=$_.charvals_limited;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_posicode.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.text1xoffset=0;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.encoding="auto";$_.version="a";$_.checkoffset=0;$_.raw=false;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _2H=$k[--$j];$_.options=_2H;var _2I=$k[--$j];$_.barcode=_2I;if($_.barcode.length>500){$k[$j++]="bwipp.posicodeinputTooLong#18245";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($ne($_.version,"a")&&$ne($_.version,"b")&&$ne($_.version,"limiteda")&&$ne($_.version,"limitedb")){$k[$j++]="bwipp.posicodeInvalidVersion#18262";$k[$j++]="The version must be either a, b, limiteda or limitedb";bwipp_raiseerror()}var _2S=$eq($_.version,"a")||$eq($_.version,"b")?$_.posicode_charmapsnormal:$_.posicode_charmapslimited;$_.charmaps=_2S;var _2Y=$eq($_.version,"a")||$eq($_.version,"b")?$_.charvals_normal:$_.charvals_limited;$_.charvals=_2Y;$_.set0=$g($_.charvals,0);$_.set1=$g($_.charvals,1);$_.set2=$g($_.charvals,2);if($eq($_.version,"limiteda")||$eq($_.version,"limitedb")){for(var _2k=0,_2j=$_.barcode.length-1;_2k<=_2j;_2k+=1){if(!$has($_.set0,$g($G($_.barcode,_2k,1),0))){$k[$j++]="bwipp.posicodeBadCharacter#18281";$k[$j++]="Posicode limited must contain only digits, capital letters, and the symbols - and .";bwipp_raiseerror()}}}if($_.raw){$_.encoding="raw"}if($eq($_.encoding,"raw")){$_.cws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _31=$G($_.barcode,$_.i+1,3);$k[$j++]=_31;$k[$j++]=false;for(var _32=0,_33=_31.length;_32<_33;_32++){var _34=$g(_31,_32);if(_34<48||_34>57){$k[$j-1]=true}}var _35=$k[--$j];if(_35){$j--;break}var _36=$k[--$j];$_.cw=$i(_36);if($_.cw>45){$k[$j++]="bwipp.posicodeBadRawCodeword#18299";$k[$j++]="Raw codewords must be 0 to 45";bwipp_raiseerror()}$p($_.cws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.posicodeBadRawFormat#18306";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.cws=$G($_.cws,0,$_.j);$_.text=""}if($eq($_.encoding,"auto")){$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.posicode_fn1],["FNC2",$_.posicode_fn2],["FNC3",$_.posicode_fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _3N=$k[--$j];$_.msg=_3N;$_.msglen=$_.msg.length;$_.text=$s($_.msglen);for(var _3T=0,_3S=$_.msglen-1;_3T<=_3S;_3T+=1){$_.i=_3T;var _3Y=$g($_.msg,$_.i);var _3b=$_.text;var _3a=$_.i;if(_3Y<0){_3Y=32}$p(_3b,_3a,_3Y)}$k[$j++]=Infinity;for(var _3d=0,_3e=$_.msglen;_3d<_3e;_3d++){$k[$j++]=0}$k[$j++]=0;$_.numSA=$a();$k[$j++]=Infinity;for(var _3g=0,_3h=$_.msglen;_3g<_3h;_3g++){$k[$j++]=0}$k[$j++]=0;$_.numEA=$a();for(var _3j=$_.msglen-1;_3j>=0;_3j-=1){$_.i=_3j;if($g($_.msg,$_.i)>=0){if($g($_.msg,$_.i)>=128){$p($_.numEA,$_.i,$g($_.numEA,$_.i+1)+1)}else{$p($_.numSA,$_.i,$g($_.numSA,$_.i+1)+1)}}}$_.ea=false;$k[$j++]=Infinity;for(var _42=0,_41=$_.msglen-1;_42<=_41;_42+=1){$_.i=_42;$_.c=$g($_.msg,$_.i);if(!($_.ea!=$_.c<128)&&$_.c>=0){var _4D=$_.ea?$_.numSA:$_.numEA;var _4E=$g(_4D,$_.i);var _4H=$f(_4E+$_.i)==$_.msglen?3:5;if(_4E<_4H){$k[$j++]=$_.posicode_fn4}else{$_.ea=!$_.ea;$k[$j++]=$_.posicode_fn4;$k[$j++]=$_.posicode_fn4}}if($_.c>=0){$k[$j++]=$_.c&127}else{$k[$j++]=$_.c}}$as($a($m()));var _4O=$k[--$j];$_.msg=_4O;$j--;$_.msglen=$_.msg.length;$_.enc=function(){var _4Q=$k[--$j];var _4R=$k[--$j];$p($_.cws,$_.j,$g(_4Q,_4R));$_.j=$_.j+1};$_.cws=$a($_.msglen*2);$_.i=0;$_.j=0;$_.cset="set0";for(;;){if($_.i==$_.msglen){break}for(;;){$_.char1=$g($_.msg,$_.i);var _4i=$_.i+1<$_.msglen?$g($_.msg,$_.i+1):-99;$_.char2=_4i;var _4l=$_[$_.cset];if($has(_4l,$_.char1)){var _4q=$_[$_.cset];$k[$j++]=$_.char1;$k[$j++]=_4q;$_.enc();$_.i=$_.i+1;break}if($has($_.set2,$_.char1)){var _4w=$_[$_.cset];$k[$j++]=$_.posicode_sf2;$k[$j++]=_4w;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.set2;$_.enc();$_.i=$_.i+1;break}var _51=$_[$_.cset];if(!$has(_51,$_.char2)){if($eq($_.cset,"set0")){var _56=$_[$_.cset];$k[$j++]=$_.posicode_la1;$k[$j++]=_56;$_.enc();$_.cset="set1"}else{var _58=$_[$_.cset];$k[$j++]=$_.posicode_la0;$k[$j++]=_58;$_.enc();$_.cset="set0"}break}else{if($eq($_.cset,"set0")){var _5B=$_[$_.cset];$k[$j++]=$_.posicode_sf1;$k[$j++]=_5B;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.set1;$_.enc()}else{var _5F=$_[$_.cset];$k[$j++]=$_.posicode_sf0;$k[$j++]=_5F;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.set0;$_.enc()}$_.i=$_.i+1;break}break}}$_.cws=$G($_.cws,0,$_.j)}$_.encs=$g($_.posicode_encmaps,$_.version);$_.v=0;var _5O=$_.cws;for(var _5P=0,_5Q=_5O.length;_5P<_5Q;_5P++){$_.cw=$g(_5O,_5P);for(var _5S=0;_5S<6;_5S++){if((($_.cw^$_.v)&1)!=0){$_.v=$_.v^7682}$_.v=$flr($_.v/2);$_.cw=$flr($_.cw/2)}}$_.v=$_.v+$_.checkoffset;if($eq($_.version,"limiteda")||$eq($_.version,"limitedb")){$_.v=$_.v&1023;if($_.v>824&&$_.v<853){$_.v=$_.v+292}}else{$_.v=($_.v&1023)+45}$_.d=$a([2,2,2,2,2,2]);$_.r=0;$_.c=0;$_.w=0;$_.sum=0;for(;;){if($_.sum==$_.v){break}$_.t=$f($_.sum+$g($g($_.posicode_c2w,$_.r),$_.c));if($_.t==$_.v){$_.w=$_.w+1;$p($_.d,$_.r,$_.w+2);$_.sum=$_.t}if($_.t>$_.v){$p($_.d,$_.r,$_.w+2);$_.r=$_.r+1;$_.w=0}if($_.t<$_.v){$_.c=$_.c+1;$_.w=$_.w+1;$_.sum=$_.t}}var _6B=20;for(var _66=0;_66<=4;_66+=1){_6B=$f(_6B-$g($_.d,_66))}$p($_.d,5,_6B);if($eq($_.version,"b")||$eq($_.version,"limitedb")){$k[$j++]=Infinity;var _6E=$_.d;for(var _6F=0,_6G=_6E.length;_6F<_6G;_6F++){$k[$j++]=$g(_6E,_6F)+1}$_.d=$a()}$_.cbs=$Z($s(12),"111111111111");for(var _6K=5;_6K>=0;_6K-=1){$_.i=_6K;$p($_.cbs,(5-$_.i)*2+1,$g($_.d,$_.i)+47)}$_.sbs=$s($_.cws.length*6+31);var _6U=$g($_.encs,$_.encs.length-2);$P($_.sbs,0,_6U);$_.j=_6U.length;for(var _6Y=0,_6X=$_.cws.length-1;_6Y<=_6X;_6Y+=1){$_.i=_6Y;$P($_.sbs,$_.j,$g($_.encs,$g($_.cws,$_.i)));$_.j=$_.j+6}$P($_.sbs,$_.j,$_.cbs);$_.j=$_.j+12;var _6n=$g($_.encs,$_.encs.length-1);$P($_.sbs,$_.j,_6n);$_.j=_6n.length+$_.j;$_.sbs=$G($_.sbs,0,$_.j);if($_.height==-1){$_.height=1}$k[$j++]=Infinity;var _6z=[];var _6v=$_.sbs;for(var _6w=0,_6x=_6v.length;_6w<_6x;_6w++){_6z[_6w]=$g(_6v,_6w)-48}_6z=$a(_6z);var _74=[];for(var _71=0,_72=~~(($_.sbs.length+1)/2);_71<_72;_71++){_74[_71]=$_.height}_74=$a(_74);var _78=[];for(var _76=0,_77=~~(($_.sbs.length+1)/2);_76<_77;_76++){_78[_76]=0}_78=$a(_78);var _7G=new Map([["a",0],["b",0],["limiteda",12],["limitedb",13]]);var _7J=new Map([["a",0],["b",0],["limiteda",12],["limitedb",13]]);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_6z;$k[$j++]="bhs";$k[$j++]=_74;$k[$j++]="bbs";$k[$j++]=_78;$k[$j++]="txt";$k[$j++]=$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="borderleft";$k[$j++]=$g(_7G,$_.version);$k[$j++]="borderright";$k[$j++]=$g(_7J,$_.version);$k[$j++]="opt";$k[$j++]=$_.options;var _7N=$d();$k[$j++]=_7N;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_codablockf(){if(!bwipp_codablockf.globals){var $__=$_;$_=bwipp_codablockf.globals={};$_.codablockf_swa=-1;$_.codablockf_swb=-2;$_.codablockf_swc=-3;$_.codablockf_sft=-4;$_.codablockf_fn1=-5;$_.codablockf_fn2=-6;$_.codablockf_fn3=-7;$_.codablockf_fn4=-8;$_.codablockf_sta=-9;$_.codablockf_stp=-10;$_.swa=$_.codablockf_swa;$_.swb=$_.codablockf_swb;$_.swc=$_.codablockf_swc;$_.sft=$_.codablockf_sft;$_.fn1=$_.codablockf_fn1;$_.fn2=$_.codablockf_fn2;$_.fn3=$_.codablockf_fn3;$_.fn4=$_.codablockf_fn4;$_.sta=$_.codablockf_sta;$_.stp=$_.codablockf_stp;$_.codablockf_charmaps=$a([$a([32,32,"00"]),$a(["!","!","01"]),$a(['"','"',"02"]),$a(["#","#","03"]),$a(["$","$","04"]),$a(["%","%","05"]),$a(["&","&","06"]),$a(["'","'","07"]),$a([40,40,"08"]),$a([41,41,"09"]),$a(["*","*","10"]),$a(["+","+","11"]),$a([",",",","12"]),$a(["-","-","13"]),$a([".",".","14"]),$a(["/","/","15"]),$a(["0","0","16"]),$a(["1","1","17"]),$a(["2","2","18"]),$a(["3","3","19"]),$a(["4","4","20"]),$a(["5","5","21"]),$a(["6","6","22"]),$a(["7","7","23"]),$a(["8","8","24"]),$a(["9","9","25"]),$a([":",":","26"]),$a([";",";","27"]),$a(["<","<","28"]),$a(["=","=","29"]),$a([">",">","30"]),$a(["?","?","31"]),$a(["@","@","32"]),$a(["A","A","33"]),$a(["B","B","34"]),$a(["C","C","35"]),$a(["D","D","36"]),$a(["E","E","37"]),$a(["F","F","38"]),$a(["G","G","39"]),$a(["H","H","40"]),$a(["I","I","41"]),$a(["J","J","42"]),$a(["K","K","43"]),$a(["L","L","44"]),$a(["M","M","45"]),$a(["N","N","46"]),$a(["O","O","47"]),$a(["P","P","48"]),$a(["Q","Q","49"]),$a(["R","R","50"]),$a(["S","S","51"]),$a(["T","T","52"]),$a(["U","U","53"]),$a(["V","V","54"]),$a(["W","W","55"]),$a(["X","X","56"]),$a(["Y","Y","57"]),$a(["Z","Z","58"]),$a(["[","[","59"]),$a([92,92,"60"]),$a(["]","]","61"]),$a(["^","^","62"]),$a(["_","_","63"]),$a([0,"`","64"]),$a([1,"a","65"]),$a([2,"b","66"]),$a([3,"c","67"]),$a([4,"d","68"]),$a([5,"e","69"]),$a([6,"f","70"]),$a([7,"g","71"]),$a([8,"h","72"]),$a([9,"i","73"]),$a([10,"j","74"]),$a([11,"k","75"]),$a([12,"l","76"]),$a([13,"m","77"]),$a([14,"n","78"]),$a([15,"o","79"]),$a([16,"p","80"]),$a([17,"q","81"]),$a([18,"r","82"]),$a([19,"s","83"]),$a([20,"t","84"]),$a([21,"u","85"]),$a([22,"v","86"]),$a([23,"w","87"]),$a([24,"x","88"]),$a([25,"y","89"]),$a([26,"z","90"]),$a([27,"{","91"]),$a([28,"|","92"]),$a([29,"}","93"]),$a([30,"~","94"]),$a([31,127,"95"]),$a([$_.fn3,$_.fn3,"96"]),$a([$_.fn2,$_.fn2,"97"]),$a([$_.sft,$_.sft,"98"]),$a([$_.swc,$_.swc,"99"]),$a([$_.swb,$_.fn4,$_.swb]),$a([$_.fn4,$_.swa,$_.swa]),$a([$_.fn1,$_.fn1,$_.fn1]),$a([$_.sta,$_.sta,$_.sta]),$a([$_.stp,$_.stp,$_.stp])]);$_.codablockf_encs=$a(["212222","222122","222221","121223","121322","131222","122213","122312","132212","221213","221312","231212","112232","122132","122231","113222","123122","123221","223211","221132","221231","213212","223112","312131","311222","321122","321221","312212","322112","322211","212123","212321","232121","111323","131123","131321","112313","132113","132311","211313","231113","231311","112133","112331","132131","113123","113321","133121","313121","211331","231131","213113","213311","213131","311123","311321","331121","312113","312311","332111","314111","221411","431111","111224","111422","121124","121421","141122","141221","112214","112412","122114","122411","142112","142211","241211","221114","413111","241112","134111","111242","121142","121241","114212","124112","124211","411212","421112","421211","212141","214121","412121","111143","111341","131141","114113","114311","411113","411311","113141","114131","311141","411131","211412","2331112"]);$_.charvals=$a([new Map,new Map,new Map]);for(var _27=0,_26=$_.codablockf_charmaps.length-1;_27<=_26;_27+=1){$_.i=_27;$_.encs=$g($_.codablockf_charmaps,$_.i);for(var _2A=0;_2A<=2;_2A+=1){$_.j=_2A;var _2D=$g($_.encs,$_.j);var _2E=$t(_2D);if($eq(_2E,"stringtype")){_2D=$g(_2D,0)}$p($g($_.charvals,$_.j),_2D,$_.i)}}$_.seta=$g($_.charvals,0);$_.setb=$g($_.charvals,1);$_.setc=$g($_.charvals,2);$_.charvals=$_.charvals;$k[$j++]=Infinity;for(var _2T=64;_2T<=95;_2T+=1){$k[$j++]=_2T}for(var _2U=0;_2U<=15;_2U+=1){$k[$j++]=_2U}for(var _2V=26;_2V<=63;_2V+=1){$k[$j++]=_2V}var _2W=$a();$_.abmap=_2W;var _2Y=[];for(var _2X=0;_2X<=85;_2X+=1){_2Y[_2X]=_2X}_2Y=$a(_2Y);$_.cmap=_2Y;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_codablockf.globals);$_.dontdraw=false;$_.rows=-1;$_.columns=8;$_.rowheight=10;$_.sepheight=1;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _2Z=$k[--$j];$_.options=_2Z;var _2a=$k[--$j];$_.barcode=_2a;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length>25e3){$k[$j++]="bwipp.codablockfinputTooLong#18743";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.columns<4||$_.columns>62){$k[$j++]="bwipp.codablockfBadColumns#18747";$k[$j++]="Codablock F must have 4 to 62 columns";bwipp_raiseerror()}if($_.rows!=-1&&($_.rows<2||$_.rows>44)){$k[$j++]="bwipp.codablockfBadRows#18751";$k[$j++]="Codablock F must have 2 to 44 rows";bwipp_raiseerror()}if($_.rowheight<8||$_.rowheight>50){$k[$j++]="bwipp.codablockfBadRowHeight#18755";$k[$j++]="Codablock F must have rowheight from 8 to 50";bwipp_raiseerror()}if($_.sepheight<1||$_.sepheight>5){$k[$j++]="bwipp.codablockfBadSepHeight#18759";$k[$j++]="Codablock F must have sepheight from 1 to 5";bwipp_raiseerror()}var _2q=$_.columns>=4&&$_.columns<=62?$_.columns:8;$_.c=_2q;var _2v=$_.rows>=2&&$_.rows<=44?$_.rows:-1;$_.rows=_2v;$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.codablockf_fn1],["FNC3",$_.codablockf_fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _31=$k[--$j];$_.msg=_31;$_.msglen=$_.msg.length;$_.kmsg=$_.msg;$k[$j++]=Infinity;$F($_.msg,function(){var _35=$k[$j-1];if(_35>=128){var _36=$k[--$j];$k[$j++]=$_.codablockf_fn4;$k[$j++]=_36&127}});$as($a($m()));var _39=$k[--$j];$_.msg=_39;$j--;$_.msglen=$_.msg.length;$_.numsscr=function(){$_.n=0;$_.s=0;var _3B=$k[--$j];$_.p=_3B;for(;;){if($_.p>=$_.msglen){break}if($_.p!=0){if($g($_.msg,$_.p-1)==$_.codablockf_fn4){break}}var _3K=$g($_.msg,$_.p);var _3M=$has($_.setc,_3K);$k[$j++]=_3K;if(!_3M){$j--;break}var _3N=$k[--$j];if(_3N==$_.codablockf_fn1){if($_.s%2==0){$_.s=$_.s+1}else{break}}$_.n=$_.n+1;$_.s=$_.s+1;$_.p=$_.p+1}$k[$j++]=$_.n;$k[$j++]=$_.s};$_.enca=function(){var _3W=$k[--$j];$p($_.cws,$_.j,$g($_.seta,_3W));$_.j=$_.j+1};$_.encb=function(){var _3c=$k[--$j];$p($_.cws,$_.j,$g($_.setb,_3c));$_.j=$_.j+1};$_.encc=function(){var _3h=$k[$j-1];var _3i=$t(_3h);if($ne(_3i,"arraytype")){var _3k=$k[--$j];$k[$j++]=$g($_.setc,_3k)}else{var _3m=$k[--$j];$q(_3m);var _3n=$k[--$j];var _3o=$k[--$j];$k[$j++]=$f(_3n-48+(_3o-48)*10)}var _3q=$k[--$j];$p($_.cws,$_.j,_3q);$_.j=$_.j+1};$_.anotb=function(){var _3t=$k[--$j];$k[$j++]=$has($_.seta,_3t)&&!$has($_.setb,_3t)};$_.bnota=function(){var _3y=$k[--$j];$k[$j++]=$has($_.setb,_3y)&&!$has($_.seta,_3y)};$k[$j++]=Infinity;for(var _44=0,_45=$_.msg.length;_44<_45;_44++){$k[$j++]=0}$k[$j++]=9999;$_.nextanotb=$a();$k[$j++]=Infinity;for(var _47=0,_48=$_.msg.length;_47<_48;_47++){$k[$j++]=0}$k[$j++]=9999;$_.nextbnota=$a();for(var _4A=$_.msg.length-1;_4A>=0;_4A-=1){$_.i=_4A;$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _4E=$k[--$j];if(_4E){$p($_.nextanotb,$_.i,0)}else{$p($_.nextanotb,$_.i,$g($_.nextanotb,$_.i+1)+1)}$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _4P=$k[--$j];if(_4P){$p($_.nextbnota,$_.i,0)}else{$p($_.nextbnota,$_.i,$g($_.nextbnota,$_.i+1)+1)}}$_.abeforeb=function(){var _4X=$k[--$j];$k[$j++]=$lt($g($_.nextanotb,_4X),$g($_.nextbnota,_4X))};$_.bbeforea=function(){var _4c=$k[--$j];$k[$j++]=$lt($g($_.nextbnota,_4c),$g($_.nextanotb,_4c))};$_.padrow=function(){var _4h=$k[--$j];for(var _4i=0,_4j=_4h;_4i<_4j;_4i++){for(;;){if($eq($_.cset,"seta")){$k[$j++]=$_.codablockf_swc;$_.enca();$_.cset="setc";break}if($eq($_.cset,"setb")){$k[$j++]=$_.codablockf_swc;$_.encb();$_.cset="setc";break}if($eq($_.cset,"setc")){$k[$j++]=$_.codablockf_swb;$_.encc();$_.cset="setb";break}}}};$_.encafitsrow=function(){if($_.rem<=2&&$g($_.msg,$_.i)==$_.codablockf_fn4){var _4v=$_.rem==2&&$g($_.msg,$_.i+1)<=95;$k[$j++]=_4v;if(_4v){$k[$j++]=$_.codablockf_fn4;$_.enca();$k[$j++]=$g($_.msg,$_.i+1);$_.enca();$_.i=$_.i+2}}else{$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;$k[$j++]=true}};$_.encbfitsrow=function(){if($_.rem<=2&&$g($_.msg,$_.i)==$_.codablockf_fn4){var _5C=$_.rem==2&&$g($_.msg,$_.i+1)>=32;$k[$j++]=_5C;if(_5C){$k[$j++]=$_.codablockf_fn4;$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$_.i=$_.i+2}}else{$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;$k[$j++]=true}};$_.cws=$a(($_.c+5)*44);$_.i=0;$_.j=0;$_.r=1;$_.lastrow=false;for(;;){if($_.lastrow){break}if($_.r>44){$k[$j++]="bwipp.codablockfTooBig#18923";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$k[$j++]=$_.codablockf_sta;$_.enca();if($_.i<$_.msglen){$k[$j++]=$_.i;$_.numsscr()}else{$k[$j++]=-1;$k[$j++]=-1}var _5S=$k[--$j];$_.nums=_5S;var _5T=$k[--$j];$_.nchars=_5T;for(;;){if($_.msglen==0){$k[$j++]=$_.codablockf_swb;$_.enca();$_.cset="setb";break}if($_.nums>=2){$k[$j++]=$_.codablockf_swc;$_.enca();$_.cset="setc";break}$k[$j++]=$_.i;$_.abeforeb();var _5X=$k[--$j];if(_5X){$k[$j++]=$_.codablockf_sft;$_.enca();$_.cset="seta";break}$k[$j++]=$_.codablockf_swb;$_.enca();$_.cset="setb";break}$_.j=$_.j+1;$_.endofrow=false;for(;;){$_.rem=$f($_.c+3-$_.j%($_.c+5));if($_.i==$_.msglen||$_.endofrow){break}$k[$j++]=$_.i;$_.numsscr();var _5g=$k[--$j];$_.nums=_5g;var _5h=$k[--$j];$_.nchars=_5h;var _5m=$_.nums>$_.rem*2?$_.rem*2:$_.nums;$_.remnums=_5m;for(;;){if(($eq($_.cset,"seta")||$eq($_.cset,"setb"))&&$_.remnums>=4&&$g($_.msg,$_.i)!=$_.codablockf_fn1){if($_.remnums%2==0&&$_.rem>=3){$k[$j++]=$_.codablockf_swc;if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.cset="setc";for(var _5x=0;_5x<2;_5x++){if($g($_.msg,$_.i)==$_.codablockf_fn1){$k[$j++]=$_.codablockf_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}if($_.remnums%2!=0&&$_.rem>=4){$k[$j++]=$g($_.msg,$_.i);if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.i=$_.i+1;$k[$j++]=$_.codablockf_swc;if($eq($_.cset,"seta")){$_.enca()}else{$_.encb()}$_.cset="setc";for(var _6E=0;_6E<2;_6E++){if($g($_.msg,$_.i)==$_.codablockf_fn1){$k[$j++]=$_.codablockf_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}}$k[$j++]=$eq($_.cset,"setb");$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _6R=$k[--$j];var _6S=$k[--$j];if(_6S&&_6R&&$_.rem>=2){if($_.i<$_.msglen-1){$k[$j++]=$_.i+1;$_.bbeforea();var _6X=$k[--$j];if(_6X){$k[$j++]=$_.codablockf_sft;$_.encb();$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}}$k[$j++]=$_.codablockf_swa;$_.encb();$_.cset="seta";$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}$k[$j++]=$eq($_.cset,"seta");$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _6k=$k[--$j];var _6l=$k[--$j];if(_6l&&_6k&&$_.rem>=2){if($_.i<$_.msglen-1){$k[$j++]=$_.i+1;$_.abeforeb();var _6q=$k[--$j];if(_6q){$k[$j++]=$_.codablockf_sft;$_.enca();$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}}$k[$j++]=$_.codablockf_swb;$_.enca();$_.cset="setb";$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}if($eq($_.cset,"setc")&&$_.remnums<2&&$_.rem>=2){$_.rem=$_.rem-1;$k[$j++]=$_.i;$_.abeforeb();var _74=$k[--$j];if(_74){$k[$j++]=$_.codablockf_swa;$_.encc();$_.cset="seta";$_.encafitsrow();var _75=$k[--$j];if(_75){break}}else{$k[$j++]=$_.codablockf_swb;$_.encc();$_.cset="setb";$_.encbfitsrow();var _76=$k[--$j];if(_76){break}}}if($eq($_.cset,"seta")&&$has($_.seta,$g($_.msg,$_.i))&&$_.rem>=1){$_.encafitsrow();var _7E=$k[--$j];if(_7E){break}}if($eq($_.cset,"setb")&&$has($_.setb,$g($_.msg,$_.i))&&$_.rem>=1){$_.encbfitsrow();var _7M=$k[--$j];if(_7M){break}}if($eq($_.cset,"setc")&&$_.remnums>=2&&$_.rem>=1){if($g($_.msg,$_.i)==$_.codablockf_fn1){$k[$j++]=$_.codablockf_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}break}$_.endofrow=true;break}}if(($_.r>=$_.rows||$_.rows==-1)&&$_.r>1&&$_.i==$_.msglen&&$_.rem>=2){$k[$j++]=$_.rem-2;$_.padrow();$_.j=$_.j+3;$k[$j++]=$_.codablockf_stp;$_.enca();$_.lastrow=true}else{$k[$j++]=$_.rem;$_.padrow();$_.j=$_.j+1;$k[$j++]=$_.codablockf_stp;$_.enca();$_.r=$_.r+1}}$_.cws=$G($_.cws,0,$_.j);$_.chkmsg=$a($_.kmsg.length);$_.j=0;for(var _7r=0,_7q=$_.kmsg.length-1;_7r<=_7q;_7r+=1){$_.i=_7r;$_.char=$g($_.kmsg,$_.i);if($_.char>=0){$p($_.chkmsg,$_.j,$_.char);$_.j=$_.j+1}if($_.char==$_.codablockf_fn1&&$_.i!=0){$p($_.chkmsg,$_.j,29);$_.j=$_.j+1}}$_.t1=0;$_.t2=0;$_.k1=0;$_.k2=0;for(var _87=0,_86=$_.j-1;_87<=_86;_87+=1){$_.i=_87;$_.t1=$g($_.chkmsg,$_.i)*$_.i%86;$_.t2=$f($_.t1+$g($_.chkmsg,$_.i))%86;$_.k1=$f($_.k1+$_.t2)%86;$_.k2=$f($_.k2+$_.t1)%86}var _8T=$_.cws;var _8S=$_.cws.length-4;var _8Q=$ne($_.cset,"setc")?$_.abmap:$_.cmap;$p(_8T,_8S,$g(_8Q,$_.k1));var _8d=$_.cws;var _8c=$_.cws.length-3;var _8a=$ne($_.cset,"setc")?$_.abmap:$_.cmap;$p(_8d,_8c,$g(_8a,$_.k2));var _8e=$_.cws;var _8l=2;var _8j=$g(_8e,2-1)!=99?$_.abmap:$_.cmap;$p(_8e,_8l,$g(_8j,$_.r-2));for(var _8p=1,_8o=$_.r-1;_8p<=_8o;_8p+=1){$_.i=_8p;var _8q=$_.cws;var _8r=$_.i;var _8s=$_.c;var _8t=$g(_8q,_8r*(_8s+5)+2-1);var _8z=_8r*(_8s+5)+2;var _8x=_8t!=99?$_.abmap:$_.cmap;$p(_8q,_8z,$g(_8x,$_.i+42))}for(var _93=0,_92=$_.r-1;_93<=_92;_93+=1){$_.rcws=$G($_.cws,_93*($_.c+5),$_.c+4);$_.csum=$g($_.rcws,0);for(var _9C=1,_9B=$_.rcws.length-2;_9C<=_9B;_9C+=1){$_.i=_9C;$_.csum=$f($_.csum+$g($_.rcws,$_.i)*$_.i)}$p($_.rcws,$_.rcws.length-1,$_.csum%103)}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#19142";$k[$j++]=$_.cws;bwipp_raiseerror()}$_.rowbits=$a($_.r);for(var _9S=0,_9R=$_.r-1;_9S<=_9R;_9S+=1){$_.i=_9S;$k[$j++]=Infinity;var _9X=$G($_.cws,$_.i*($_.c+5),$_.c+5);for(var _9Y=0,_9Z=_9X.length;_9Y<_9Z;_9Y++){$F($g($_.codablockf_encs,$g(_9X,_9Y)),function(){$k[$j-1]-=48})}$_.sbs=$a();$k[$j++]=Infinity;var _9d=$_.sbs;$k[$j++]=0;for(var _9e=0,_9f=_9d.length;_9e<_9f;_9e++){var _9h=$k[$j-1];var _9i=_9h==1?0:1;for(var _9j=0,_9k=$g(_9d,_9e);_9j<_9k;_9j++){$k[$j++]=_9i}}$as($a($m()-1));var _9n=$k[--$j];$j--;$k[$j-1]=_9n;$k[$j++]=$_.rowbits;$k[$j++]=$_.i;var _9t=$k[$j-3];var _9u=$k[$j-1];var _9v=$k[$j-2];$j-=3;$p(_9v,_9u,_9t)}$_.symwid=$_.c*11+57;$_.symhgt=$_.sepheight*($_.r+1)+$_.r*$_.rowheight;$_.numcomprows=$_.r*2+1;$k[$j++]=$_.symwid*$_.numcomprows;try{var _A4=$k[--$j];$k[$j++]=$a(_A4);var _A6=false}catch(e){$k[$j++]=true}if(_A6){$k[$j-1]="bwipp.codablockfSymbolTooLarge#19168";$k[$j++]="The symbol size exceeds the implementation limits";bwipp_raiseerror()}var _A7=$k[--$j];$_.pixs=_A7;$k[$j++]=Infinity;for(var _A9=0,_AA=$_.symwid;_A9<_AA;_A9++){$k[$j++]=1}$_.allone=$a();$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;for(var _AC=0,_AD=$_.symwid-24;_AC<_AD;_AC++){$k[$j++]=1}$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=1;$_.seprow=$a();$_.rowmult=$a($_.numcomprows);$_.pos=0;$_.ri=0;$P($_.pixs,$_.pos,$_.allone);$_.pos=$f($_.pos+$_.symwid);$p($_.rowmult,$_.ri,$_.sepheight);$_.ri=$_.ri+1;for(var _AR=0,_AQ=$_.r-2;_AR<=_AQ;_AR+=1){$_.i=_AR;$P($_.pixs,$_.pos,$g($_.rowbits,$_.i));$_.pos=$f($_.pos+$_.symwid);$p($_.rowmult,$_.ri,$_.rowheight);$_.ri=$_.ri+1;$P($_.pixs,$_.pos,$_.seprow);$_.pos=$f($_.pos+$_.symwid);$p($_.rowmult,$_.ri,$_.sepheight);$_.ri=$_.ri+1}$P($_.pixs,$_.pos,$g($_.rowbits,$_.r-1));$_.pos=$f($_.pos+$_.symwid);$p($_.rowmult,$_.ri,$_.rowheight);$_.ri=$_.ri+1;$P($_.pixs,$_.pos,$_.allone);$p($_.rowmult,$_.ri,$_.sepheight);$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.symwid],["pixy",$_.symhgt],["rowmult",$_.rowmult],["height",$_.symhgt/72],["width",$_.symwid/72],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_code16k(){if(!bwipp_code16k.globals){var $__=$_;$_=bwipp_code16k.globals={};$_.code16k_swa=-1;$_.code16k_swb=-2;$_.code16k_swc=-3;$_.code16k_sa1=-4;$_.code16k_sb1=-5;$_.code16k_sc1=-6;$_.code16k_sa2=-7;$_.code16k_sb2=-8;$_.code16k_sc2=-9;$_.code16k_pad=-10;$_.code16k_sb3=-11;$_.code16k_sc3=-12;$_.code16k_fn1=-13;$_.code16k_fn2=-14;$_.code16k_fn3=-15;$_.code16k_fn4=-16;$_.swa=$_.code16k_swa;$_.swb=$_.code16k_swb;$_.swc=$_.code16k_swc;$_.sa1=$_.code16k_sa1;$_.sb1=$_.code16k_sb1;$_.sc1=$_.code16k_sc1;$_.sa2=$_.code16k_sa2;$_.sb2=$_.code16k_sb2;$_.sc2=$_.code16k_sc2;$_.pad=$_.code16k_pad;$_.sb3=$_.code16k_sb3;$_.sc3=$_.code16k_sc3;$_.fn1=$_.code16k_fn1;$_.fn2=$_.code16k_fn2;$_.fn3=$_.code16k_fn3;$_.fn4=$_.code16k_fn4;$_.code16k_charmaps=$a([$a([32,32,"00"]),$a(["!","!","01"]),$a(['"','"',"02"]),$a(["#","#","03"]),$a(["$","$","04"]),$a(["%","%","05"]),$a(["&","&","06"]),$a(["'","'","07"]),$a([40,40,"08"]),$a([41,41,"09"]),$a(["*","*","10"]),$a(["+","+","11"]),$a([",",",","12"]),$a(["-","-","13"]),$a([".",".","14"]),$a(["/","/","15"]),$a(["0","0","16"]),$a(["1","1","17"]),$a(["2","2","18"]),$a(["3","3","19"]),$a(["4","4","20"]),$a(["5","5","21"]),$a(["6","6","22"]),$a(["7","7","23"]),$a(["8","8","24"]),$a(["9","9","25"]),$a([":",":","26"]),$a([";",";","27"]),$a(["<","<","28"]),$a(["=","=","29"]),$a([">",">","30"]),$a(["?","?","31"]),$a(["@","@","32"]),$a(["A","A","33"]),$a(["B","B","34"]),$a(["C","C","35"]),$a(["D","D","36"]),$a(["E","E","37"]),$a(["F","F","38"]),$a(["G","G","39"]),$a(["H","H","40"]),$a(["I","I","41"]),$a(["J","J","42"]),$a(["K","K","43"]),$a(["L","L","44"]),$a(["M","M","45"]),$a(["N","N","46"]),$a(["O","O","47"]),$a(["P","P","48"]),$a(["Q","Q","49"]),$a(["R","R","50"]),$a(["S","S","51"]),$a(["T","T","52"]),$a(["U","U","53"]),$a(["V","V","54"]),$a(["W","W","55"]),$a(["X","X","56"]),$a(["Y","Y","57"]),$a(["Z","Z","58"]),$a(["[","[","59"]),$a([92,92,"60"]),$a(["]","]","61"]),$a(["^","^","62"]),$a(["_","_","63"]),$a([0,"`","64"]),$a([1,"a","65"]),$a([2,"b","66"]),$a([3,"c","67"]),$a([4,"d","68"]),$a([5,"e","69"]),$a([6,"f","70"]),$a([7,"g","71"]),$a([8,"h","72"]),$a([9,"i","73"]),$a([10,"j","74"]),$a([11,"k","75"]),$a([12,"l","76"]),$a([13,"m","77"]),$a([14,"n","78"]),$a([15,"o","79"]),$a([16,"p","80"]),$a([17,"q","81"]),$a([18,"r","82"]),$a([19,"s","83"]),$a([20,"t","84"]),$a([21,"u","85"]),$a([22,"v","86"]),$a([23,"w","87"]),$a([24,"x","88"]),$a([25,"y","89"]),$a([26,"z","90"]),$a([27,"{","91"]),$a([28,"|","92"]),$a([29,"}","93"]),$a([30,"~","94"]),$a([31,127,"95"]),$a([$_.fn3,$_.fn3,"96"]),$a([$_.fn2,$_.fn2,"97"]),$a([$_.sb1,$_.sa1,"98"]),$a([$_.swc,$_.swc,"99"]),$a([$_.swb,$_.fn4,$_.swb]),$a([$_.fn4,$_.swa,$_.swa]),$a([$_.fn1,$_.fn1,$_.fn1]),$a([$_.pad,$_.pad,$_.pad]),$a([$_.sb2,$_.sa2,$_.sb1]),$a([$_.sc2,$_.sc2,$_.sb2]),$a([$_.sc3,$_.sc3,$_.sb3])]);$_.code16k_metrics=$a([$a([2,7]),$a([3,12]),$a([4,17]),$a([5,22]),$a([6,27]),$a([7,32]),$a([8,37]),$a([9,42]),$a([10,47]),$a([11,52]),$a([12,57]),$a([13,62]),$a([14,67]),$a([15,72]),$a([16,77])]);$_.code16k_encs=$a(["212222","222122","222221","121223","121322","131222","122213","122312","132212","221213","221312","231212","112232","122132","122231","113222","123122","123221","223211","221132","221231","213212","223112","312131","311222","321122","321221","312212","322112","322211","212123","212321","232121","111323","131123","131321","112313","132113","132311","211313","231113","231311","112133","112331","132131","113123","113321","133121","313121","211331","231131","213113","213311","213131","311123","311321","331121","312113","312311","332111","314111","221411","431111","111224","111422","121124","121421","141122","141221","112214","112412","122114","122411","142112","142211","241211","221114","413111","241112","134111","111242","121142","121241","114212","124112","124211","411212","421112","421211","212141","214121","412121","111143","111341","131141","114113","114311","411113","411311","113141","114131","311141","411131","211412","211214","211232","211133"]);$_.code16k_startencs=$a(["3211","2221","2122","1411","1132","1231","1114","3112","3211","2221","2122","1411","1132","1231","1114","3112"]);$_.code16k_stopencsodd=$a(["3211","2221","2122","1411","1132","1231","1114","3112","1132","1231","1114","3112","3211","2221","2122","1411"]);$_.code16k_stopencseven=$a(["2122","1411","1132","1231","1114","3112","1132","1231","1114","3112","3211","2221","2122","1411","3211","2221"]);$_.charvals=$a([new Map,new Map,new Map]);for(var _2Y=0,_2X=$_.code16k_charmaps.length-1;_2Y<=_2X;_2Y+=1){$_.i=_2Y;$_.encs=$g($_.code16k_charmaps,$_.i);for(var _2b=0;_2b<=2;_2b+=1){$_.j=_2b;var _2e=$g($_.encs,$_.j);var _2f=$t(_2e);if($eq(_2f,"stringtype")){_2e=$g(_2e,0)}$p($g($_.charvals,$_.j),_2e,$_.i)}}$_.charvals=$_.charvals;$_.seta=$g($_.charvals,0);$_.setb=$g($_.charvals,1);$_.setc=$g($_.charvals,2);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code16k.globals);$_.dontdraw=false;$_.sam=-1;$_.rows=0;$_.rowheight=8;$_.sepheight=1;$_.raw=false;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _2u=$k[--$j];$_.options=_2u;var _2v=$k[--$j];$_.barcode=_2v;if($_.barcode.length>500){$k[$j++]="bwipp.code16kinputTooLong#19446";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.rows!=0&&($_.rows<2||$_.rows>16)){$k[$j++]="bwipp.code16kBadRows#19463";$k[$j++]="Code 16K must have 2 to 16 rows";bwipp_raiseerror()}if($_.rowheight<8||$_.rowheight>50){$k[$j++]="bwipp.code16kBadRowHeight#19467";$k[$j++]="Code 16K must have rowheight from 8 to 50";bwipp_raiseerror()}if($_.sepheight<1||$_.sepheight>5){$k[$j++]="bwipp.code16kBadSepHeight#19471";$k[$j++]="Code 16K must have sepheight from 1 to 5";bwipp_raiseerror()}if($_.sam!=-1){$k[$j++]=true;for(var _37=0;_37<1;_37++){var _38=$_.sam;if(_38<12||_38>99){$k[$j-1]=false;break}if($_.sam%10<2){$k[$j-1]=false;break}if(~~($_.sam/10)>$_.sam%10){$k[$j-1]=false;break}}var _3C=$k[--$j];if(!_3C){$k[$j++]="bwipp.code16kBadSAM#19482";$k[$j++]='SAM must be formatted as "NM" for Nth of M symbols, from 2 to 9 symbols';bwipp_raiseerror()}}if($_.sam!=-1){$_.rows=16}if($_.raw){$_.mode=0;$_.cws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _3O=$G($_.barcode,$_.i+1,3);$k[$j++]=_3O;$k[$j++]=false;for(var _3P=0,_3Q=_3O.length;_3P<_3Q;_3P++){var _3R=$g(_3O,_3P);if(_3R<48||_3R>57){$k[$j-1]=true}}var _3S=$k[--$j];if(_3S){$j--;break}var _3T=$k[--$j];$_.cw=$i(_3T);if($_.cw>106){$k[$j++]="bwipp.code16kBadRawCodeword#19501";$k[$j++]="Raw codewords must be 0 to 106";bwipp_raiseerror()}$p($_.cws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.code16kBadRawFormat#19508";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.cws=$G($_.cws,0,$_.j)}else{$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.code16k_fn1],["FNC2",$_.code16k_fn2],["FNC3",$_.code16k_fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _3j=$k[--$j];$_.msg=_3j;$_.msglen=$_.msg.length;$k[$j++]=Infinity;for(var _3m=0,_3n=$_.msglen;_3m<_3n;_3m++){$k[$j++]=0}$k[$j++]=0;$_.numSA=$a();$k[$j++]=Infinity;for(var _3p=0,_3q=$_.msglen;_3p<_3q;_3p++){$k[$j++]=0}$k[$j++]=0;$_.numEA=$a();for(var _3s=$_.msglen-1;_3s>=0;_3s-=1){$_.i=_3s;if($g($_.msg,$_.i)>=0){if($g($_.msg,$_.i)>=128){$p($_.numEA,$_.i,$g($_.numEA,$_.i+1)+1)}else{$p($_.numSA,$_.i,$g($_.numSA,$_.i+1)+1)}}}$_.ea=false;$k[$j++]=Infinity;for(var _4B=0,_4A=$_.msglen-1;_4B<=_4A;_4B+=1){$_.i=_4B;$_.c=$g($_.msg,$_.i);if(!($_.ea!=$_.c<128)&&$_.c>=0){var _4M=$_.ea?$_.numSA:$_.numEA;var _4N=$g(_4M,$_.i);var _4Q=$f(_4N+$_.i)==$_.msglen?3:5;if(_4N<_4Q){$k[$j++]=$_.code16k_fn4}else{$_.ea=!$_.ea;$k[$j++]=$_.code16k_fn4;$k[$j++]=$_.code16k_fn4}}if($_.c>=0){$k[$j++]=$_.c&127}else{$k[$j++]=$_.c}}$as($a($m()));var _4X=$k[--$j];$_.msg=_4X;$j--;$_.msglen=$_.msg.length;$_.numsscr=function(){$_.n=0;$_.s=0;var _4Z=$k[--$j];$_.p=_4Z;for(;;){if($_.p>=$_.msglen){break}var _4e=$g($_.msg,$_.p);var _4g=$has($_.setc,_4e);$k[$j++]=_4e;if(!_4g){$j--;break}var _4h=$k[--$j];if(_4h==$_.code16k_fn1){if($_.s%2==0){$_.s=$_.s+1}else{break}}$_.n=$_.n+1;$_.s=$_.s+1;$_.p=$_.p+1}$k[$j++]=$_.n;$k[$j++]=$_.s};$_.enca=function(){var _4q=$k[--$j];$p($_.cws,$_.j,$g($_.seta,_4q));$_.j=$_.j+1};$_.encb=function(){var _4w=$k[--$j];$p($_.cws,$_.j,$g($_.setb,_4w));$_.j=$_.j+1};$_.encc=function(){var _51=$k[$j-1];var _52=$t(_51);if($ne(_52,"arraytype")){var _54=$k[--$j];$k[$j++]=$g($_.setc,_54)}else{var _56=$k[--$j];$q(_56);var _57=$k[--$j];var _58=$k[--$j];$k[$j++]=$f(_57-48+(_58-48)*10)}var _5A=$k[--$j];$p($_.cws,$_.j,_5A);$_.j=$_.j+1};$_.anotb=function(){var _5D=$k[--$j];$k[$j++]=$has($_.seta,_5D)&&!$has($_.setb,_5D)};$_.bnota=function(){var _5I=$k[--$j];$k[$j++]=$has($_.setb,_5I)&&!$has($_.seta,_5I)};$k[$j++]=Infinity;for(var _5O=0,_5P=$_.msg.length;_5O<_5P;_5O++){$k[$j++]=0}$k[$j++]=9999;$_.nextanotb=$a();$k[$j++]=Infinity;for(var _5R=0,_5S=$_.msg.length;_5R<_5S;_5R++){$k[$j++]=0}$k[$j++]=9999;$_.nextbnota=$a();for(var _5U=$_.msg.length-1;_5U>=0;_5U-=1){$_.i=_5U;$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _5Y=$k[--$j];if(_5Y){$p($_.nextanotb,$_.i,0)}else{$p($_.nextanotb,$_.i,$g($_.nextanotb,$_.i+1)+1)}$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _5j=$k[--$j];if(_5j){$p($_.nextbnota,$_.i,0)}else{$p($_.nextbnota,$_.i,$g($_.nextbnota,$_.i+1)+1)}}$_.abeforeb=function(){var _5r=$k[--$j];$k[$j++]=$lt($g($_.nextanotb,_5r),$g($_.nextbnota,_5r))};$_.bbeforea=function(){var _5w=$k[--$j];$k[$j++]=$lt($g($_.nextbnota,_5w),$g($_.nextanotb,_5w))};$_.cws=$a($_.barcode.length*2+3);$_.i=0;$_.j=0;for(;;){if($_.sam!=-1){$p($_.cws,0,~~($_.sam/10)-1);$p($_.cws,1,$_.sam%10-1);$_.j=2;$_.cset="setb";$_.mode=7;break}if($_.msglen==0){$_.cset="setb";$_.mode=1;break}if($_.msglen>=2){$k[$j++]=$g($_.msg,0)==$_.code16k_fn1;$k[$j++]=1;$_.numsscr();var _6C=$k[--$j];$j--;var _6F=$k[--$j];if(_6F&&_6C>=2){$_.cset="setc";$_.mode=4;$_.i=1;break}}if($g($_.msg,0)==$_.code16k_fn1){$_.cset="setb";$_.mode=3;$_.i=1;break}if($_.msglen>=2){$k[$j++]=0;$_.numsscr();var _6J=$k[--$j];$j--;if(_6J>=2&&_6J%2==0){$_.cset="setc";$_.mode=2;break}$k[$j++]=0;$_.numsscr();var _6M=$k[--$j];$j--;if(_6M>=3&&_6M%2==1){$k[$j++]=$g($_.msg,0);$_.encb();$_.cset="setc";$_.mode=5;$_.i=1;break}$k[$j++]=$has($_.setb,$g($_.msg,0));$k[$j++]=1;$_.numsscr();var _6V=$k[--$j];$j--;var _6Y=$k[--$j];if(_6Y&&(_6V>=2&&_6V%2==0)){$k[$j++]=$g($_.msg,0);$_.encb();$_.cset="setc";$_.mode=5;$_.i=1;break}$k[$j++]=$has($_.setb,$g($_.msg,0));$k[$j++]=1;$_.numsscr();var _6f=$k[--$j];$j--;var _6i=$k[--$j];if(_6i&&(_6f>=3&&_6f%2==1)){$k[$j++]=$g($_.msg,0);$_.encb();$k[$j++]=$g($_.msg,1);$_.encb();$_.cset="setc";$_.mode=6;$_.i=2;break}$k[$j++]=$has($_.setb,$g($_.msg,0))&&$has($_.setb,$g($_.msg,1));$k[$j++]=2;$_.numsscr();var _6v=$k[--$j];$j--;var _6y=$k[--$j];if(_6y&&(_6v>=2&&_6v%2==0)){$k[$j++]=$g($_.msg,0);$_.encb();$k[$j++]=$g($_.msg,1);$_.encb();$_.cset="setc";$_.mode=6;$_.i=2;break}}$k[$j++]=0;$_.abeforeb();var _73=$k[--$j];if(_73){$_.cset="seta";$_.mode=0;break}$_.cset="setb";$_.mode=1;break}for(;;){if($_.i==$_.msglen){break}$k[$j++]=$_.i;$_.numsscr();var _77=$k[--$j];$_.nums=_77;var _78=$k[--$j];$_.nchars=_78;for(;;){if($eq($_.cset,"seta")){if($_.i<$_.msglen-1){$k[$j++]=$g($_.msg,$_.i);$_.bnota();$k[$j++]=$_.i+1;$_.abeforeb();var _7G=$k[--$j];var _7H=$k[--$j];if(_7H&&_7G){$k[$j++]=$_.code16k_sb1;$_.enca();$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}}if($_.i<$_.msglen-2){$k[$j++]=$g($_.msg,$_.i);$_.bnota();$k[$j++]=$g($_.msg,$_.i+1);$_.bnota();var _7U=$k[--$j];var _7V=$k[--$j];$k[$j++]=$an(_7V,_7U);$k[$j++]=$_.i+2;$_.abeforeb();var _7X=$k[--$j];var _7Y=$k[--$j];if(_7Y&&_7X){$k[$j++]=$_.code16k_sb2;$_.enca();$k[$j++]=$g($_.msg,$_.i);$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$_.i=$_.i+2;break}}$k[$j++]=$g($_.msg,$_.i);$_.bnota();var _7j=$k[--$j];if(_7j){$k[$j++]=$_.code16k_swb;$_.enca();$_.cset="setb";break}if($_.i<$_.msglen-4){if($_.nums==4&&$has($_.seta,$g($_.msg,$_.i+4))){$k[$j++]=$_.code16k_sc2;$_.enca();for(var _7s=0;_7s<2;_7s++){if($g($_.msg,$_.i)==$_.code16k_fn1){$k[$j++]=$_.code16k_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}}if($_.i<$_.msglen-6){if($_.nums==6&&$has($_.seta,$g($_.msg,$_.i+6))){$k[$j++]=$_.code16k_sc3;$_.enca();for(var _89=0;_89<3;_89++){if($g($_.msg,$_.i)==$_.code16k_fn1){$k[$j++]=$_.code16k_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}}if($_.nums>=4&&$_.nums%2==0){$k[$j++]=$_.code16k_swc;$_.enca();$_.cset="setc";break}$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}if($eq($_.cset,"setb")){if($_.i<$_.msglen-1){$k[$j++]=$g($_.msg,$_.i);$_.anotb();$k[$j++]=$_.i+1;$_.bbeforea();var _8V=$k[--$j];var _8W=$k[--$j];if(_8W&&_8V){$k[$j++]=$_.code16k_sa1;$_.encb();$k[$j++]=$g($_.msg,$_.i);$_.enca();$_.i=$_.i+1;break}}if($_.i<$_.msglen-2){$k[$j++]=$g($_.msg,$_.i);$_.anotb();$k[$j++]=$g($_.msg,$_.i+1);$_.anotb();var _8j=$k[--$j];var _8k=$k[--$j];$k[$j++]=$an(_8k,_8j);$k[$j++]=$_.i+2;$_.bbeforea();var _8m=$k[--$j];var _8n=$k[--$j];if(_8n&&_8m){$k[$j++]=$_.code16k_sa2;$_.encb();$k[$j++]=$g($_.msg,$_.i);$_.enca();$k[$j++]=$g($_.msg,$_.i+1);$_.enca();$_.i=$_.i+2;break}}$k[$j++]=$g($_.msg,$_.i);$_.anotb();var _8y=$k[--$j];if(_8y){$k[$j++]=$_.code16k_swa;$_.encb();$_.cset="seta";break}if($_.i<$_.msglen-4){if($_.nums==4&&$has($_.setb,$g($_.msg,$_.i+4))){$k[$j++]=$_.code16k_sc2;$_.encb();for(var _97=0;_97<2;_97++){if($g($_.msg,$_.i)==$_.code16k_fn1){$k[$j++]=$_.code16k_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}}if($_.i<$_.msglen-6){if($_.nums==6&&$has($_.setb,$g($_.msg,$_.i+6))){$k[$j++]=$_.code16k_sc3;$_.encb();for(var _9O=0;_9O<3;_9O++){if($g($_.msg,$_.i)==$_.code16k_fn1){$k[$j++]=$_.code16k_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}}break}}if($_.nums>=4&&$_.nums%2==0){$k[$j++]=$_.code16k_swc;$_.encb();$_.cset="setc";break}$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}if($eq($_.cset,"setc")){if($_.nums>=2){if($g($_.msg,$_.i)==$_.code16k_fn1){$k[$j++]=$_.code16k_fn1;$_.encc();$_.i=$_.i+1}else{$k[$j++]=$G($_.msg,$_.i,2);$_.encc();$_.i=$_.i+2}break}if($_.i<$_.msglen-1){$k[$j++]=$has($_.setb,$g($_.msg,$_.i));$k[$j++]=$_.i+1;$_.numsscr();var _9v=$k[--$j];$j--;var _9y=$k[--$j];if(_9y&&(_9v>=2&&_9v%2==0)){$k[$j++]=$_.code16k_sb1;$_.encc();$k[$j++]=$g($_.msg,$_.i);$_.encb();$_.i=$_.i+1;break}}if($_.i<$_.msglen-1){$k[$j++]=$has($_.setb,$g($_.msg,$_.i));$k[$j++]=$_.i+1;$_.numsscr();var _AB=$k[--$j];$j--;var _AE=$k[--$j];if(_AE&&(_AB>=3&&_AB%2==1)){$k[$j++]=$_.code16k_sb2;$_.encc();$k[$j++]=$g($_.msg,$_.i);$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$_.i=$_.i+2;break}}if($_.i<$_.msglen-2){$k[$j++]=$has($_.setb,$g($_.msg,$_.i))&&$has($_.setb,$g($_.msg,$_.i+1));$k[$j++]=$_.i+2;$_.numsscr();var _AZ=$k[--$j];$j--;var _Ac=$k[--$j];if(_Ac&&(_AZ>=2&&_AZ%2==0)){$k[$j++]=$_.code16k_sb2;$_.encc();$k[$j++]=$g($_.msg,$_.i);$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$_.i=$_.i+2;break}}if($_.i<$_.msglen-3){$k[$j++]=$has($_.setb,$g($_.msg,$_.i))&&$has($_.setb,$g($_.msg,$_.i+1));$k[$j++]=$_.i+2;$_.numsscr();var _Ax=$k[--$j];$j--;var _B0=$k[--$j];if(_B0&&(_Ax>=3&&_Ax%2==1)){$k[$j++]=$_.code16k_sb3;$_.encc();$k[$j++]=$g($_.msg,$_.i);$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$k[$j++]=$g($_.msg,$_.i+2);$_.encb();$_.i=$_.i+3;break}}if($_.i<$_.msglen-3){$k[$j++]=$has($_.setb,$g($_.msg,$_.i))&&$has($_.setb,$g($_.msg,$_.i+1))&&$has($_.setb,$g($_.msg,$_.i+2));$k[$j++]=$_.i+3;$_.numsscr();var _BT=$k[--$j];$j--;var _BW=$k[--$j];if(_BW&&(_BT>=2&&_BT%2==0)){$k[$j++]=$_.code16k_sb3;$_.encc();$k[$j++]=$g($_.msg,$_.i);$_.encb();$k[$j++]=$g($_.msg,$_.i+1);$_.encb();$k[$j++]=$g($_.msg,$_.i+2);$_.encb();$_.i=$_.i+3;break}}$k[$j++]=$_.i;$_.abeforeb();var _Bi=$k[--$j];if(_Bi){$k[$j++]=$_.code16k_swa;$_.encc();$_.cset="seta";break}$k[$j++]=$_.code16k_swb;$_.encc();$_.cset="setb";break}break}}$_.cws=$G($_.cws,0,$_.j)}$_.urows=$_.rows;for(var _Bn=0,_Bo=$_.code16k_metrics.length;_Bn<_Bo;_Bn++){$_.m=$g($_.code16k_metrics,_Bn);$_.r=$g($_.m,0);$_.dcws=$g($_.m,1);$_.okay=true;if($_.urows!=0&&$_.urows!=$_.r){$_.okay=false}if($_.cws.length>$_.dcws){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.code16kNoValidSymbol#19996";$k[$j++]="Maximum length exceeded or data too large for given options";bwipp_raiseerror()}$k[$j++]=Infinity;$q($_.cws);for(var _C4=0,_C5=$f($_.dcws-$_.cws.length);_C4<_C5;_C4++){$k[$j++]=103}$_.cws=$a();$k[$j++]=Infinity;$k[$j++]=$f(($_.r-2)*7+$_.mode);$q($_.cws);$_.cws=$a();var _CF=0;for(var _CB=0,_CA=$_.dcws;_CB<=_CA;_CB+=1){_CF=$f(_CF+(_CB+2)*$g($_.cws,_CB))}$_.c1=_CF%107;var _CO=0;for(var _CI=0,_CH=$_.dcws;_CI<=_CH;_CI+=1){_CO=$f(_CO+(_CI+1)*$g($_.cws,_CI))}$_.c2=$f(_CO+$_.c1*($_.dcws+2))%107;$k[$j++]=Infinity;$q($_.cws);$k[$j++]=$_.c1;$k[$j++]=$_.c2;$_.cws=$a();if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#20020";$k[$j++]=$_.cws;bwipp_raiseerror()}if($_.sam==-1||~~($_.sam/10)%2==1){$_.stopencs=$_.code16k_stopencsodd}else{$_.stopencs=$_.code16k_stopencseven}$_.rowbits=$a($_.r);for(var _Cb=0,_Ca=$_.r-1;_Cb<=_Ca;_Cb+=1){$_.i=_Cb;$k[$j++]=Infinity;$k[$j++]=10;$F($g($_.code16k_startencs,$_.i),function(){$k[$j-1]-=48});var _Ch=$G($_.cws,$_.i*5,5);$k[$j++]=1;for(var _Ci=0,_Cj=_Ch.length;_Ci<_Cj;_Ci++){$F($g($_.code16k_encs,$g(_Ch,_Ci)),function(){$k[$j-1]-=48})}$F($g($_.stopencs,$_.i),function(){$k[$j-1]-=48});$k[$j++]=1;$_.sbs=$a();$k[$j++]=Infinity;var _Cr=$_.sbs;$k[$j++]=1;for(var _Cs=0,_Ct=_Cr.length;_Cs<_Ct;_Cs++){var _Cv=$k[$j-1];var _Cw=_Cv==0?1:0;for(var _Cx=0,_Cy=$g(_Cr,_Cs);_Cx<_Cy;_Cx++){$k[$j++]=_Cw}}$as($a($m()-1));var _D1=$k[--$j];$j--;$k[$j-1]=_D1;$k[$j++]=$_.rowbits;$k[$j++]=$_.i;var _D7=$k[$j-3];var _D8=$k[$j-1];var _D9=$k[$j-2];$j-=3;$p(_D9,_D8,_D7)}$_.symhgt=$f($_.sepheight*($_.r+1)+$_.r*$_.rowheight);$_.numcomprows=$_.r*2+1;$k[$j++]=81*$_.numcomprows;try{var _DG=$k[--$j];$k[$j++]=$a(_DG);var _DI=false}catch(e){$k[$j++]=true}if(_DI){$k[$j-1]="bwipp.code16kSymbolTooLarge#20056";$k[$j++]="The symbol size exceeds the implementation limits";bwipp_raiseerror()}var _DJ=$k[--$j];$_.pixs=_DJ;$k[$j++]=Infinity;for(var _DK=0;_DK<81;_DK++){$k[$j++]=1}$_.allone=$a();$k[$j++]=Infinity;for(var _DL=0;_DL<10;_DL++){$k[$j++]=0}for(var _DM=0;_DM<70;_DM++){$k[$j++]=1}$k[$j++]=0;$_.seprow=$a();$_.rowmult=$a($_.numcomprows);$_.pos=0;$_.ri=0;$P($_.pixs,$_.pos,$_.allone);$_.pos=$_.pos+81;$p($_.rowmult,$_.ri,$_.sepheight);$_.ri=$_.ri+1;for(var _DZ=0,_DY=$_.r-2;_DZ<=_DY;_DZ+=1){$_.i=_DZ;$P($_.pixs,$_.pos,$g($_.rowbits,$_.i));$_.pos=$_.pos+81;$p($_.rowmult,$_.ri,$_.rowheight);$_.ri=$_.ri+1;$P($_.pixs,$_.pos,$_.seprow);$_.pos=$_.pos+81;$p($_.rowmult,$_.ri,$_.sepheight);$_.ri=$_.ri+1}$P($_.pixs,$_.pos,$g($_.rowbits,$_.r-1));$_.pos=$_.pos+81;$p($_.rowmult,$_.ri,$_.rowheight);$_.ri=$_.ri+1;$P($_.pixs,$_.pos,$_.allone);$p($_.rowmult,$_.ri,$_.sepheight);$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",81],["pixy",$_.symhgt],["rowmult",$_.rowmult],["height",$_.symhgt/72],["width",81/72],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_code49(){if(!bwipp_code49.globals){var $__=$_;$_=bwipp_code49.globals={};$_.code49_s1=-1;$_.code49_s2=-2;$_.code49_fn1=-3;$_.code49_fn2=-4;$_.code49_fn3=-5;$_.code49_ns=-6;$_.code49_charmap=$a(["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","-","."," ","$","/","+","%",$_.code49_s1,$_.code49_s2,$_.code49_fn1,$_.code49_fn2,$_.code49_fn3,$_.code49_ns]);$_.code49_combos=$a(["1 ","1A","1B","1C","1D","1E","1F","1G","1H","1I","1J","1K","1L","1M","1N","1O","1P","1Q","1R","1S","1T","1U","1V","1W","1X","1Y","1Z","11","12","13","14","15"," ","16","17","18"," $"," %","19","10","1-","1.","1$"," +","1/"," -"," ."," /"," 0"," 1"," 2"," 3"," 4"," 5"," 6"," 7"," 8"," 9","1+","21","22","23","24","25","26"," A"," B"," C"," D"," E"," F"," G"," H"," I"," J"," K"," L"," M"," N"," O"," P"," Q"," R"," S"," T"," U"," V"," W"," X"," Y"," Z","27","28","29","20","2-","2.","2A","2B","2C","2D","2E","2F","2G","2H","2I","2J","2K","2L","2M","2N","2O","2P","2Q","2R","2S","2T","2U","2V","2W","2X","2Y","2Z","2$","2/","2+","2%","2 "]);$_.code49_samval=$a([12,22,13,23,33,14,24,34,44,15,25,35,45,55,16,26,36,46,56,66,17,27,37,47,57,67,77,18,28,38,48,58,68,78,88,19,29,39,49,59,69,79,89,99]);$_.code49_metrics=$a([$a([2,9]),$a([3,16]),$a([4,23]),$a([5,30]),$a([6,37]),$a([7,42]),$a([8,49])]);var _B=$a([1,9,31,26,2,12,17,23,37,18,22,6,27,44,15,43,39,11,13,5,41,33,36,8,4,32,3,19,40,25,29,10,24,30]);$k[$j++]=_B;$k[$j++]=Infinity;$k[$j++]=20;$q($G(_B,0,32));var _F=$a();$_.code49_weightx=_F;var _G=$k[$j-1];$k[$j++]=Infinity;$k[$j++]=16;$q($G(_G,1,32));var _K=$a();$_.code49_weighty=_K;var _M=$k[--$j];$k[$j++]=Infinity;$k[$j++]=38;$q($G(_M,2,32));var _O=$a();$_.code49_weightz=_O;$_.code49_patterns=$a([$a(["11521132","25112131","14212132","25121221","14221222","12412132","23321221","12421222","21521221","15112222","15121312","13312222","24221311","13321312","11512222","22421311","11521312","25112311","14212312","23312311","12412312","21512311","16121131","14321131","12521131","15212131","15221221","13412131","13421221","11612131","16112221","16121311","14312221","14321311","12512221","12521311","15212311","13412311","11612311","11131135","31131133","51131131","21122134","41122132","21131224","41131222","11113135","31113133","51113131","11122225","31122223","51122221","11131315","31131313","51131311","21113224","41113222","21122314","41122312","11113315","31113313","51113311","12131134","32131132","21231133","41231131","22122133","42122131","11222134","22131223","42131221","11231224","31231222","12113134","32113132","12122224","32122222","12131314","32131312","21231313","41231311","22113223","42113221","11213224","22122313","42122311","11222314","31222312","12113314","32113312","21213313","41213311","13131133","33131131","22231132","11331133","31331131","23122132","12222133","23131222","12231223","32231221","21331222","13113133","33113131","13122223","33122221","11313133","13131313","33131311","11322223","22231312","11331313","31331311","23113222","12213223","23122312","12222313","32222311","21322312","13113313","33113311","22213312","11313313","31313311","14131132","23231131","12331132","21431131","24122131","13222132","24131221","13231222","11422132","22331221","11431222","14113132","14122222","12313132","14131312","12322222","23231311","12331312","21431311","24113221","13213222","24122311","13222312","11413222","22322311","11422312","14113312","23213311","12313312","21413311","15131131","13331131","14222131","14231221","12422131","12431221","15113131","15122221","13313131","15131311","13322221","11513131","13331311","11522221","14213221","14222311","12413221","12422311","15113311","13313311","11513311","11141134","31141132","21132133","41132131","21141223","41141221","11123134","31123132","11132224","31132222","11141314","31141312","21114133","41114131","21123223","41123221","21132313","41132311","11114224","31114222","11123314","31123312","21114313","41114311","12141133","32141131","21241132","22132132","11232133","22141222","11241223","31241221","12123133","32123131","12132223","32132221","12141313","32141311","21241312","22114132","11214133","22123222","11223223","22132312","11232313","31232311","12114223","32114221","12123313","32123311","21223312","22114312","11214313","31214311","13141132","22241131","11341132","23132131","12232132","23141221","12241222","21341221","13123132","13132222","11323132","13141312","11332222","22241311","11341312","23114131","12214132","23123221","12223222","23132311","12232312","21332311","13114222","13123312","11314222","22223311","11323312","23114311","12214312","21314311","14141131","12341131","13232131","13241221","11432131","14123131","14132221","12323131","14141311","12332221","12341311","13214131","13223221","11414131","13232311","11423221","11432311","14114221","14123311","12314221","12323311","13214311","11414311","11151133","31151131","21142132","21151222","11133133","31133131","11142223","31142221","11151313","31151311","21124132","21133222","21142312","11115133","31115131","11124223","31124221","11133313","31133311","21115222","21124312","12151132","21251131","22142131","11242132","22151221","11251222","12133132","12142222","12151312","21251311","22124131","11224132","22133221","11233222","22142311","11242312","12115132","12124222","12133312","21233311","22115221","11215222","22124311","11224312","13151131","12242131","12251221","13133131","13142221","11333131","13151311","11342221","12224131","12233221","12242311","13115131","13124221","11315131","13133311","11324221","11333311","12215221","12224311","11161132","21152131","21161221","11143132","11152222","11161312","21134131","21143221","21152311","11125132","11134222","11143312","21116131","21125221","21134311","12161131","11252131","12143131","12152221","12161311","11234131","11243221","11252311","12125131","12134221","12143311","11216131","11225221","11234311","11111236","31111234","51111232","21111325","41111323","61111321","11111416","31111414","51111412","31211143","51211141","12111235","32111233","52111231","21211234","41211232","22111324","42111322","11211325","31211323","51211321","12111415","32111413","52111411","21211414","41211412","12211144","32211142","21311143","41311141","13111234","33111232","22211233","42211231","11311234","31311232","23111323","43111321","12211324","32211322","21311323","41311321","13111414","33111412","22211413","42211411","11311414","31311412","13211143","33211141","22311142","11411143","31411141","14111233","34111231","23211232","12311233","32311231","21411232","24111322","13211323","33211321","22311322","11411323","31411321","14111413","34111411","23211412","12311413","32311411","21411412","14211142","23311141","12411142","21511141","15111232","24211231","13311232","22411231","11511232","25111321","14211322","23311321","12411322","21511321","15111412","24211411","13311412","22411411","11511412","15211141","13411141","11611141","16111231","14311231","12511231","15211321","13411321","11611321","16111411","14311411","12511411","21121144","41121142","11112145","31112143","51112141","11121235","31121233","51121231","21112234","41112232","21121324","41121322","11112325","31112323","51112321","11121415","31121413","51121411","21112414","41112412","22121143","42121141","11221144","31221142","12112144","32112142","12121234","32121232","21221233","41221231","22112233","42112231","11212234","22121323","42121321","11221324","31221322","12112324","32112322","12121414","32121412","21221413","41221411","22112413","42112411","11212414","31212412","23121142","12221143","32221141","21321142","13112143","33112141","13121233","33121231","11312143","22221232","11321233","31321231","23112232","12212233","23121322","12221323","32221321","21321322","13112323","33112321","13121413","33121411","11312323","22221412","11321413","31321411","23112412","12212413","32212411","21312412","24121141","13221142","22321141","11421142","14112142","14121232","12312142","23221231","12321232","21421231","24112231","13212232","24121321","13221322","11412232","22321321","11421322","14112322","14121412","12312322","23221411","12321412","21421411","24112411","13212412","22312411","11412412","14221141","12421141","15112141","15121231","13312141","13321231","11512141","11521231","14212231","14221321","12412231","12421321","15112321","15121411","13312321","13321411","11512321","11521411","14212411","12412411","21131143","41131141","11122144","31122142","11131234","31131232","21113143","41113141","21122233","41122231","21131323","41131321","11113234","31113232","11122324","31122322","11131414","31131412","21113323","41113321","21122413","41122411","11113414","31113412","22131142","11231143","31231141","12122143","32122141","12131233","32131231","21231232","22113142","11213143","22122232","11222233","22131322","11231323","31231321","12113233","32113231","12122323","32122321","12131413","32131411","21231412","22113322","11213323","22122412","11222413","31222411","12113413","32113411","21213412","23131141","12231142","21331141","13122142","13131232","11322142","22231231","11331232","23113141","12213142","23122231","12222232","23131321","12231322","21331321","13113232","13122322","11313232","13131412","11322322","22231411","11331412","23113321","12213322","23122411","12222412","21322411","13113412","22213411","11313412","13231141","11431141","14122141","14131231","12322141","12331231","13213141","13222231","11413141","13231321","11422231","11431321","14113231","14122321","12313231","14131411","12322321","12331411","13213321","13222411","11413321","11422411","14113411","12313411","21141142","11132143","31132141","11141233","31141231","21123142","21132232","21141322","11114143","31114141","11123233","31123231","11132323","31132321","11141413","31141411","21114232","21123322","21132412","11114323","31114321","11123413","31123411","22141141","11241142","12132142","12141232","21241231","22123141","11223142","22132231","11232232","22141321","11241322","12114142","12123232","12132322","12141412","21241411","22114231","11214232","22123321","11223322","22132411","11232412","12114322","12123412","21223411","12241141","13132141","13141231","11332141","11341231","12223141","12232231","12241321","13114141","13123231","11314141","13132321","11323231","13141411","11332321","11341411","12214231","12223321","12232411","13114321","13123411","11314321","11323411","21151141","11142142","11151232","21133141","21142231","21151321","11124142","11133232","11142322","11151412","21115141","21124231","21133321","21142411","11115232","11124322","11133412","11251141","12142141","12151231","11233141","11242231","11251321","12124141","12133231","12142321","12151411","11215141","11224231","11233321","11242411","12115231","12124321","12133411","11152141","11161231","11134141","11143231","11152321","11161411","11116141","11125231","11134321","11143411","21111244","41111242","11111335","31111333","51111331","21111424","41111422","11111515","31111513","51111511","21211153","41211151","22111243","42111241","11211244","31211242","12111334","32111332","21211333","41211331","22111423","42111421","11211424","31211422","12111514","32111512","21211513","41211511","22211152","11311153","31311151","23111242","12211243","32211241","21311242","13111333","33111331","22211332","11311333","31311331","23111422","12211423","32211421","21311422","13111513","33111511","22211512","11311513","31311511","23211151","12311152","21411151","24111241","13211242","22311241","11411242","14111332","23211331","12311332","21411331","24111421","13211422","22311421","11411422","14111512","23211511","12311512","21411511","13311151","11511151","14211241","12411241","15111331","13311331","11511331","14211421","12411421","15111511","13311511","11511511","31121152","21112153","41112151","21121243","41121241","11112244","31112242","11121334","31121332","21112333","41112331","21121423","41121421","11112424","31112422","11121514","31121512","21112513","41112511","12121153","32121151","21221152","22112152","11212153","22121242","11221243","31221241","12112243","32112241","12121333","32121331","21221332","22112332","11212333","22121422","11221423","31221421","12112423","32112421","12121513","32121511","21221512","22112512","11212513","31212511","13121152","22221151","11321152","23112151","12212152","23121241","12221242","21321241","13112242","13121332","11312242","22221331","11321332","23112331","12212332","23121421","12221422","21321421","13112422","13121512","11312422","22221511","11321512","23112511","12212512","21312511","14121151","12321151","13212151","13221241","11412151","11421241","14112241","14121331","12312241","12321331","13212331","13221421","11412331","11421421","14112421","14121511","12312421","12321511","13212511","11412511","11131153","31131151","21122152","21131242","11113153","31113151","11122243","31122241","11131333","31131331","21113242","21122332","21131422","11113333","31113331","11122423","31122421","11131513","31131511","21113422","21122512","12131152","21231151","22122151","11222152","22131241","11231242","12113152","12122242","12131332","21231331","22113241","11213242","22122331","11222332","22131421","11231422","12113332","12122422","12131512","21231511","22113421","11213422","22122511","11222512","13131151","11331151","12222151","12231241","13113151","13122241","11313151","13131331","11322241","11331331","12213241","12222331","12231421","13113331","13122421","11313331","13131511","11322421","11331511","12213421","12222511","11141152","21132151","21141241","11123152","11132242","11141332","21114151","21123241","21132331","21141421","11114242","11123332","11132422","11141512","21114331","21123421","21132511","12141151","11232151","11241241","12123151","12132241","12141331","11214151","11223241","11232331","11241421","12114241","12123331","12132421","12141511","11214331","11223421","11232511","11151151","11133151","11142241","11151331","11115151","11124241","11133331","11142421","11151511","11111254","31111252","21111343","41111341","11111434","31111432","21111523","41111521","11111614","31111612","31211161","12111253","32111251","21211252","22111342","11211343","31211341","12111433","32111431","21211432","22111522","11211523","31211521","12111613","32111611","21211612","12211162","21311161","13111252","22211251","11311252","23111341","12211342","21311341","13111432","22211431","11311432","23111521","12211522","21311521","13111612","22211611","11311612","13211161","11411161","14111251","12311251","13211341","11411341","14111431","12311431","13211521","11411521","14111611","12311611","21121162","11112163","31112161","11121253","31121251","21112252","21121342","11112343","31112341","11121433","31121431","21112432","21121522","11112523","31112521","11121613","31121611","22121161","11221162","12112162","12121252","21221251","22112251","11212252","22121341","11221342","12112342","12121432","21221431","22112431","11212432","22121521","11221522","12112522","12121612","21221611","12221161","13112161","13121251","11312161","11321251","32121115","52121113","21221116","41221114","61221112","22112116","42112114","31212115","51212113","13121116","33121114","22221115","42221113","11321116","31321114","51321112","23112115","43112113","12212116","32212114","52212112","21312115","41312113","61312111","14121115","34121113","23221114","43221112","12321115","32321113","52321111","21421114","41421112","24112114","13212115","33212113","22312114","42312112","11412115","31412113","51412111","15121114","24221113","13321114","33321112","22421113","42421111","11521114","31521112","25112113","14212114","34212112","23312113","43312111","12412114","32412112","21512113","41512111","16121113","25221112","14321113","34321111","23421112","12521113","32521111","15212113","24312112","13412113","33412111","22512112","11612113","31612111","31131115","51131113","21122116","41122114","61122112","31113115","51113113","12131116","32131114","52131112","21231115","41231113","61231111","22122115","42122113","11222116","31222114","51222112","12113116","32113114","52113112","21213115","41213113","61213111","13131115","33131113","22231114","42231112","11331115","31331113","51331111","23122114","43122112","12222115","32222113","52222111","21322114","41322112","13113115","33113113","22213114","42213112","11313115","31313113","51313111","14131114","34131112","23231113","43231111","12331114","32331112","21431113","41431111","24122113","13222114","33222112","22322113","42322111","11422114","31422112","14113114","34113112","23213113","43213111","12313114","32313112","21413113","41413111","15131113","24231112","13331113","33331111","22431112","25122112","14222113","34222111","23322112","12422113","32422111","21522112","15113113","24213112","13313113","33313111","22413112","11513113","31513111","16131112","25231111","14331112","23431111","15222112","24322111","13422112","22522111","16113112","25213111","14313112","23413111","12513112","21613111","11141116","31141114","51141112","21132115","41132113","61132111","11123116","31123114","51123112","21114115","41114113","61114111","12141115","32141113","52141111","21241114","41241112","22132114","42132112","11232115","31232113","51232111","12123115","32123113","52123111","21223114","41223112","22114114","42114112","11214115","31214113","51214111","13141114","33141112","22241113","42241111","11341114","31341112","23132113","43132111","12232114","32232112","21332113","41332111","13123114","33123112","22223113","42223111","11323114","31323112","23114113","43114111","12214114","32214112","21314113","41314111","14141113","34141111","23241112","12341113","32341111","24132112","13232113","33232111","22332112","11432113","31432111","14123113","34123111","23223112","12323113","32323111","21423112","24114112","13214113","33214111","22314112","11414113","31414111","15141112","24241111","13341112","25132111","14232112","23332111","12432112","15123112","24223111","13323112","22423111","11523112","25114111","14214112","23314111","12414112","21514111","16141111","14341111","15232111","13432111","16123111","14323111","12523111","15214111","13414111","11614111","11151115","31151113","51151111","21142114","41142112","11133115","31133113","51133111","21124114","41124112","11115115","31115113","51115111","12151114","32151112","21251113","41251111","22142113","42142111","11242114","31242112","12133114","32133112","21233113","41233111","22124113","42124111","11224114","31224112","12115114","32115112","21215113","41215111","13151113","33151111","22251112","23142112","12242113","32242111","21342112","13133113","33133111","22233112","11333113","31333111","23124112","12224113","32224111","21324112","13115113","33115111","22215112","11315113","31315111","14151112","23251111","24142111","13242112","22342111","14133112","23233111","12333112","21433111","24124111","13224112","22324111","11424112","14115112","23215111","12315112","21415111","15151111","14242111","15133111","13333111","14224111","12424111","15115111","13315111","11515111","11161114","31161112","21152113","41152111","11143114","31143112","21134113","41134111","11125114","31125112","21116113","41116111","12161113","32161111","22152112","11252113","31252111","12143113","32143111","21243112","22134112","11234113","31234111","12125113","32125111","21225112","22116112","11216113","31216111","13161112","23152111","12252112","13143112","22243111","11343112","23134111","12234112","21334111","13125112","22225111","11325112","23116111","12216112","21316111","14161111","13252111","14143111","12343111","13234111","11434111","14125111","12325111","13216111","11416111","31111216","51111214","31211125","51211123","32111215","52111213","21211216","41211214","61211212","12211126","32211124","52211122","21311125","41311123","61311121","13111216","33111214","22211215","42211213","11311216","31311214","51311212","13211125","33211123","22311124","42311122","11411125","31411123","51411121","14111215","34111213","23211214","43211212","12311215","32311213","52311211","21411214","41411212","14211124","34211122","23311123","43311121","12411124","32411122","21511123","41511121","15111214","24211213","13311214","33311212","22411213","42411211","11511214","31511212","15211123","24311122","13411123","33411121","22511122","11611123","31611121","16111213","25211212","14311213","34311211","23411212","12511213","32511211","21611212","21121126","41121124","61121122","31112125","51112123","31121215","51121213","21112216","41112214","61112212","22121125","42121123","11221126","31221124","51221122","12112126","32112124","52112122","12121216","32121214","52121212","21221215","41221213","61221211","22112215","42112213","11212216","31212214","51212212","23121124","43121122","12221125","32221123","52221121","21321124","41321122","13112125","33112123","13121215","33121213","11312125","22221214","42221212","11321215","31321213","51321211","23112214","43112212","12212215","32212213","52212211","21312214","41312212","24121123","13221124","33221122","22321123","42321121","11421124","31421122","14112124","34112122","14121214","34121212","12312124","23221213","43221211","12321214","32321212","21421213","41421211","24112213","13212214","33212212","22312213","42312211","11412214","31412212","25121122","14221123","34221121","23321122","12421123","32421121","21521122","15112123","15121213","13312123","24221212","13321213","33321211","11512123","22421212","11521213","31521211","25112212","14212213","34212211","23312212","12412213","32412211","21512212","15221122","24321121","13421122","22521121","16112122","16121212","14312122","25221211","14321212","12512122","23421211","12521212","15212212","24312211","13412212","22512211","11612212","21131125","41131123","61131121","11122126","31122124","51122122","11131216","31131214","51131212","21113125","41113123","61113121","21122215","41122213","61122211","11113216","31113214","51113212","22131124","42131122","11231125","31231123","51231121","12122125","32122123","52122121","12131215","32131213","52131211","21231214","41231212","22113124","42113122","11213125","22122214","42122212","11222215","31222213","51222211","12113215","32113213","52113211","21213214","41213212","23131123","43131121","12231124","32231122","21331123","41331121","13122124","33122122","13131214","33131212","11322124","22231213","42231211","11331214","31331212","23113123","43113121","12213124","23122213","43122211","12222214","32222212","21322213","41322211","13113214","33113212","22213213","42213211","11313214","31313212","24131122","13231123","33231121","22331122","11431123","31431121","14122123","34122121","14131213","34131211","12322123","23231212","12331213","32331211","21431212","24113122","13213123","24122212","13222213","33222211","11413123","22322212","11422213","31422211","14113213","34113211","23213212","12313213","32313211","21413212","25131121","14231122","23331121","12431122","15122122","15131212","13322122","24231211","13331212","11522122","22431211","25113121","14213122","25122211","14222212","12413122","23322211","12422212","21522211","15113212","24213211","13313212","22413211","11513212","15231121","13431121","16122121","16131211","14322121","14331211","12522121","15213121","15222211","13413121","13422211","11613121","16113211","14313211","12513211","21141124","41141122","11132125","31132123","51132121","11141215","31141213","51141211","21123124","41123122","21132214","41132212","11114125","31114123","51114121","11123215","31123213","51123211","21114214","41114212","22141123","42141121","11241124","31241122","12132124","32132122","12141214","32141212","21241213","41241211","22123123","42123121","11223124","22132213","42132211","11232214","31232212","12114124","32114122","12123214","32123212","21223213","41223211","22114213","42114211","11214214","31214212","23141122","12241123","32241121","21341122","13132123","33132121","13141213","33141211","11332123","22241212","11341213","31341211","23123122","12223123","23132212","12232213","32232211","21332212","13114123","33114121","13123213","33123211","11314123","22223212","11323213","31323211","23114212","12214213","32214211","21314212","24141121","13241122","22341121","14132122","14141212","12332122","23241211","12341212","24123121","13223122","24132211","13232212","11423122","22332211","11432212","14114122","14123212","12314122","23223211","12323212","21423211","24114211","13214212","22314211","11414212","14241121","15132121","15141211","13332121","13341211","14223121","14232211","12423121","12432211","15114121","15123211","13314121","13323211","11514121","11523211","14214211","12414211","21151123","41151121","11142124","31142122","11151214","31151212","21133123","41133121","21142213","41142211","11124124","31124122","11133214","31133212","21115123","41115121","21124213","41124211","11115214","31115212","22151122","11251123","31251121","12142123","32142121","12151213","32151211","21251212","22133122","11233123","22142212","11242213","31242211","12124123","32124121","12133213","32133211","21233212","22115122","11215123","22124212","11224213","31224211","12115213","32115211","21215212","23151121","12251122","13142122","13151212","11342122","22251211","23133121","12233122","23142211","12242212","21342211","13124122","13133212","11324122","22233211","11333212","23115121","12215122","23124211","12224212","21324211","13115212","22215211","11315212","13251121","14142121","14151211","12342121","13233121","13242211","11433121","14124121","14133211","12324121","12333211","13215121","13224211","11415121","11424211","14115211","12315211","21161122","11152123","31152121","11161213","31161211","21143122","21152212","11134123","31134121","11143213","31143211","21125122","21134212","11116123","31116121","11125213","31125211","22161121","12152122","12161212","22143121","11243122","22152211","11252212","12134122","12143212","21243211","22125121","11225122","22134211","11234212","12116122","12125212","21225211","13152121","13161211","12243121","12252211","13134121","13143211","11334121","11343211","12225121","12234211","13116121","13125211","11316121","11325211","21111226","41111224","61111222","31111315","51111313","21211135","41211133","61211131","22111225","42111223","11211226","31211224","51211222","12111316","32111314","52111312","21211315","41211313","61211311","22211134","42211132","11311135","31311133","51311131","23111224","43111222","12211225","32211223","52211221","21311224","41311222","13111315","33111313","22211314","42211312","11311315","31311313","51311311","23211133","43211131","12311134","32311132","21411133","41411131","24111223","13211224","33211222","22311223","42311221","11411224","31411222","14111314","34111312","23211313","43211311","12311314","32311312","21411313","41411311","24211132","13311133","33311131","22411132","11511133","31511131","25111222","14211223","34211221","23311222","12411223","32411221","21511222","15111313","24211312","13311313","33311311","22411312","11511313","31511311","25211131","14311132","23411131","12511132","21611131","15211222","24311221","13411222","22511221","11611222","16111312","25211311","14311312","23411311","12511312","21611311","31121134","51121132","21112135","41112133","61112131","21121225","41121223","61121221","11112226","31112224","51112222","11121316","31121314","51121312","21112315","41112313","61112311","12121135","32121133","52121131","21221134","41221132","22112134","42112132","11212135","22121224","42121222","11221225","31221223","51221221","12112225","32112223","52112221","12121315","32121313","52121311","21221314","41221312","22112314","42112312","11212315","31212313","51212311","13121134","33121132","22221133","42221131","11321134","31321132","23112133","43112131","12212134","23121223","43121221","12221224","32221222","21321223","41321221","13112224","33112222","13121314","33121312","11312224","22221313","42221311","11321314","31321312","23112313","43112311","12212314","32212312","21312313","41312311","14121133","34121131","23221132","12321133","32321131","21421132","24112132","13212133","24121222","13221223","33221221","11412133","22321222","11421223","31421221","14112223","34112221","14121313","34121311","12312223","23221312","12321313","32321311","21421312","24112312","13212313","33212311","22312312","11412313","31412311","15121132","24221131","13321132","22421131"]),$a(["22121116","42121114","31221115","51221113","32112115","52112113","21212116","41212114","61212112","23121115","43121113","12221116","32221114","52221112","21321115","41321113","61321111","13112116","33112114","22212115","42212113","11312116","31312114","51312112","24121114","13221115","33221113","22321114","42321112","11421115","31421113","51421111","14112115","34112113","23212114","43212112","12312115","32312113","52312111","21412114","41412112","25121113","14221114","34221112","23321113","43321111","12421114","32421112","21521113","41521111","15112114","24212113","13312114","33312112","22412113","42412111","11512114","31512112","15221113","24321112","13421113","33421111","22521112","16112113","25212112","14312113","34312111","23412112","12512113","32512111","21612112","21131116","41131114","61131112","31122115","51122113","21113116","41113114","61113112","22131115","42131113","11231116","31231114","51231112","12122116","32122114","52122112","21222115","41222113","61222111","22113115","42113113","11213116","31213114","51213112","23131114","43131112","12231115","32231113","52231111","21331114","41331112","13122115","33122113","22222114","42222112","11322115","31322113","51322111","23113114","43113112","12213115","32213113","52213111","21313114","41313112","24131113","13231114","33231112","22331113","42331111","11431114","31431112","14122114","34122112","23222113","43222111","12322114","32322112","21422113","41422111","24113113","13213114","33213112","22313113","42313111","11413114","31413112","25131112","14231113","34231111","23331112","12431113","32431111","15122113","24222112","13322113","33322111","22422112","11522113","31522111","25113112","14213113","34213111","23313112","12413113","32413111","21513112","15231112","24331111","13431112","16122112","25222111","14322112","23422111","12522112","15213112","24313111","13413112","22513111","11613112","21141115","41141113","61141111","11132116","31132114","51132112","21123115","41123113","61123111","11114116","31114114","51114112","22141114","42141112","11241115","31241113","51241111","12132115","32132113","52132111","21232114","41232112","22123114","42123112","11223115","31223113","51223111","12114115","32114113","52114111","21214114","41214112","23141113","43141111","12241114","32241112","21341113","41341111","13132114","33132112","22232113","42232111","11332114","31332112","23123113","43123111","12223114","32223112","21323113","41323111","13114114","33114112","22214113","42214111","11314114","31314112","24141112","13241113","33241111","22341112","14132113","34132111","23232112","12332113","32332111","21432112","24123112","13223113","33223111","22323112","11423113","31423111","14114113","34114111","23214112","12314113","32314111","21414112","25141111","14241112","23341111","15132112","24232111","13332112","22432111","25123111","14223112","23323111","12423112","21523111","15114112","24214111","13314112","22414111","11514112","15241111","16132111","14332111","15223111","13423111","16114111","14314111","12514111","21151114","41151112","11142115","31142113","51142111","21133114","41133112","11124115","31124113","51124111","21115114","41115112","22151113","42151111","11251114","31251112","12142114","32142112","21242113","41242111","22133113","42133111","11233114","31233112","12124114","32124112","21224113","41224111","22115113","42115111","11215114","31215112","23151112","12251113","32251111","13142113","33142111","22242112","11342113","31342111","23133112","12233113","32233111","21333112","13124113","33124111","22224112","11324113","31324111","23115112","12215113","32215111","21315112","24151111","13251112","14142112","23242111","12342112","24133111","13233112","22333111","11433112","14124112","23224111","12324112","21424111","24115111","13215112","22315111","11415112","14251111","15142111","13342111","14233111","12433111","15124111","13324111","11524111","14215111","12415111","21161113","41161111","11152114","31152112","21143113","41143111","11134114","31134112","21125113","41125111","11116114","31116112","22161112","12152113","32152111","21252112","22143112","11243113","31243111","12134113","32134111","21234112","22125112","11225113","31225111","12116113","32116111","21216112","23161111","13152112","22252111","23143111","12243112","21343111","13134112","22234111","11334112","23125111","12225112","21325111","13116112","22216111","11316112","14152111","13243111","14134111","12334111","13225111","11425111","14116111","12316111","41111215","61111213","21211126","41211124","61211122","22111216","42111214","31211215","51211213","22211125","42211123","11311126","31311124","51311122","23111215","43111213","12211216","32211214","52211212","21311215","41311213","61311211","23211124","43211122","12311125","32311123","52311121","21411124","41411122","24111214","13211215","33211213","22311214","42311212","11411215","31411213","51411211","24211123","13311124","33311122","22411123","42411121","11511124","31511122","25111213","14211214","34211212","23311213","43311211","12411214","32411212","21511213","41511211","25211122","14311123","34311121","23411122","12511123","32511121","21611122","15211213","24311212","13411213","33411211","22511212","11611213","31611211","31121125","51121123","21112126","41112124","61112122","21121216","41121214","61121212","31112215","51112213","12121126","32121124","52121122","21221125","41221123","61221121","22112125","42112123","11212126","22121215","42121213","11221216","31221214","51221212","12112216","32112214","52112212","21212215","41212213","61212211","13121125","33121123","22221124","42221122","11321125","31321123","51321121","23112124","43112122","12212125","23121214","43121212","12221215","32221213","52221211","21321214","41321212","13112215","33112213","22212214","42212212","11312215","31312213","51312211","14121124","34121122","23221123","43221121","12321124","32321122","21421123","41421121","24112123","13212124","24121213","13221214","33221212","11412124","22321213","42321211","11421214","31421212","14112214","34112212","23212213","43212211","12312214","32312212","21412213","41412211","15121123","24221122","13321123","33321121","22421122","11521123","31521121","25112122","14212123","25121212","14221213","34221211","12412123","23321212","12421213","32421211","21521212","15112213","24212212","13312213","33312211","22412212","11512213","31512211","16121122","25221121","14321122","23421121","12521122","15212122","15221212","13412122","24321211","13421212","11612122","22521211","16112212","25212211","14312212","23412211","12512212","21612211","11131126","31131124","51131122","21122125","41122123","61122121","21131215","41131213","61131211","11113126","31113124","51113122","11122216","31122214","51122212","21113215","41113213","61113211","12131125","32131123","52131121","21231124","41231122","22122124","42122122","11222125","22131214","42131212","11231215","31231213","51231211","12113125","32113123","52113121","12122215","32122213","52122211","21222214","41222212","22113214","42113212","11213215","31213213","51213211","13131124","33131122","22231123","42231121","11331124","31331122","23122123","43122121","12222124","23131213","43131211","12231214","32231212","21331213","41331211","13113124","33113122","13122214","33122212","11313124","22222213","42222211","11322214","31322212","23113213","43113211","12213214","32213212","21313213","41313211","14131123","34131121","23231122","12331123","32331121","21431122","24122122","13222123","24131212","13231213","33231211","11422123","22331212","11431213","31431211","14113123","34113121","14122213","34122211","12313123","23222212","12322213","32322211","21422212","24113212","13213213","33213211","22313212","11413213","31413211","15131122","24231121","13331122","22431121","25122121","14222122","25131211","14231212","12422122","23331211","12431212","15113122","15122212","13313122","24222211","13322212","11513122","22422211","11522212","25113211","14213212","23313211","12413212","21513211","16131121","14331121","15222121","15231211","13422121","13431211","16113121","16122211","14313121","14322211","12513121","12522211","15213211","13413211","11613211","11141125","31141123","51141121","21132124","41132122","21141214","41141212","11123125","31123123","51123121","11132215","31132213","51132211","21114124","41114122","21123214","41123212","11114215","31114213","51114211","12141124","32141122","21241123","41241121","22132123","42132121","11232124","22141213","42141211","11241214","31241212","12123124","32123122","12132214","32132212","21232213","41232211","22114123","42114121","11214124","22123213","42123211","11223214","31223212","12114214","32114212","21214213","41214211","13141123","33141121","22241122","11341123","31341121","23132122","12232123","23141212","12241213","32241211","21341212","13123123","33123121","13132213","33132211","11323123","22232212","11332213","31332211","23114122","12214123","23123212","12223213","32223211","21323212","13114213","33114211","22214212","11314213","31314211","14141122","23241121","12341122","24132121","13232122","24141211","13241212","11432122","22341211","14123122","14132212","12323122","23232211","12332212","21432211","24114121","13214122","24123211","13223212","11414122","22323211","11423212","14114212","23214211","12314212","21414211","15141121","13341121","14232121","14241211","12432121","15123121","15132211","13323121","13332211","11523121","14214121","14223211","12414121","12423211","15114211","13314211","11514211","11151124","31151122","21142123","41142121","21151213","41151211","11133124","31133122","11142214","31142212","21124123","41124121","21133213","41133211","11115124","31115122","11124214","31124212","21115213","41115211","12151123","32151121","21251122","22142122","11242123","22151212","11251213","31251211","12133123","32133121","12142213","32142211","21242212","22124122","11224123","22133212","11233213","31233211","12115123","32115121","12124213","32124211","21224212","22115212","11215213","31215211","13151122","22251121","23142121","12242122","23151211","12251212","13133122","13142212","11333122","22242211","11342212","23124121","12224122","23133211","12233212","21333211","13115122","13124212","11315122","22224211","11324212","23115211","12215212","21315211","14151121","13242121","13251211","14133121","14142211","12333121","12342211","13224121","13233211","11424121","11433211","14115121","14124211","12315121","12324211","13215211","11415211","11161123","31161121","21152122","21161212","11143123","31143121","11152213","31152211","21134122","21143212","11125123","31125121","11134213","31134211","21116122","21125212","12161122","22152121","11252122","22161211","12143122","12152212","21252211","22134121","11234122","22143211","11243212","12125122","12134212","21234211","22116121","11216122","22125211","11225212","13161121","12252121","13143121","13152211","11343121","12234121","12243211","13125121","13134211","11325121","11334211","12216121","12225211","31111225","51111223","21111316","41111314","61111312","31211134","51211132","12111226","32111224","52111222","21211225","41211223","61211221","22111315","42111313","11211316","31211314","51211312","12211135","32211133","52211131","21311134","41311132","13111225","33111223","22211224","42211222","11311225","31311223","51311221","23111314","43111312","12211315","32211313","52211311","21311314","41311312","13211134","33211132","22311133","42311131","11411134","31411132","14111224","34111222","23211223","43211221","12311224","32311222","21411223","41411221","24111313","13211314","33211312","22311313","42311311","11411314","31411312","14211133","34211131","23311132","12411133","32411131","21511132","15111223","24211222","13311223","33311221","22411222","11511223","31511221","25111312","14211313","34211311","23311312","12411313","32411311","21511312","15211132","24311131","13411132","22511131","11611132","16111222","25211221","14311222","23411221","12511222","21611221","15211312","24311311","13411312","22511311","11611312","21121135","41121133","61121131","11112136","31112134","51112132","11121226","31121224","51121222","21112225","41112223","61112221","21121315","41121313","61121311","11112316","31112314","51112312","22121134","42121132","11221135","31221133","51221131","12112135","32112133","52112131","12121225","32121223","52121221","21221224","41221222","22112224","42112222","11212225","22121314","42121312","11221315","31221313","51221311","12112315","32112313","52112311","21212314","41212312","23121133","43121131","12221134","32221132","21321133","41321131","13112134","33112132","13121224","33121222","11312134","22221223","42221221","11321224","31321222","23112223","43112221","12212224","23121313","43121311","12221314","32221312","21321313","41321311","13112314","33112312","22212313","42212311","11312314","31312312","24121132","13221133","33221131","22321132","11421133","31421131","14112133","34112131","14121223","34121221","12312133","23221222","12321223","32321221","21421222","24112222","13212223","24121312","13221313","33221311","11412223","22321312","11421313","31421311","14112313","34112311","23212312","12312313","32312311","21412312","25121131","14221132","23321131","12421132","21521131","15112132","15121222","13312132","24221221","13321222","11512132","22421221","11521222","25112221","14212222","25121311","14221312","12412222","23321311","12421312","21521311","15112312","24212311","13312312","22412311","11512312","15221131","13421131","16112131","16121221","14312131","14321221","12512131","12521221","15212221","15221311","13412221","13421311","11612221","16112311","14312311","12512311","21131134","41131132","11122135","31122133","51122131","11131225","31131223","51131221","21113134","41113132","21122224","41122222","21131314","41131312","11113225","31113223","51113221","11122315","31122313","51122311","21113314","41113312","22131133","42131131","11231134","31231132","12122134","32122132","12131224","32131222","21231223","41231221","22113133","42113131","11213134","22122223","42122221","11222224","22131313","42131311","11231314","31231312","12113224","32113222","12122314","32122312","21222313","41222311","22113313","42113311","11213314","31213312","23131132","12231133","32231131","21331132","13122133","33122131","13131223","33131221","11322133","22231222","11331223","31331221","23113132","12213133","23122222","12222223","23131312","12231313","32231311","21331312","13113223","33113221","13122313","33122311","11313223","22222312","11322313","31322311","23113312","12213313","32213311","21313312","24131131","13231132","22331131","11431132","14122132","14131222","12322132","23231221","12331222","21431221","24113131","13213132","24122221","13222222","24131311","11413132","13231312","11422222","22331311","11431312","14113222","14122312","12313222","23222311","12322312","21422311","24113311","13213312","22313311","11413312","14231131","12431131","15122131","15131221","13322131","13331221","11522131","14213131","14222221","12413131","14231311","12422221","12431311","15113221","15122311","13313221","13322311","11513221","11522311","14213311","12413311","21141133","41141131","11132134","31132132","11141224","31141222","21123133","41123131","21132223","41132221","21141313","41141311","11114134","31114132","11123224","31123222","11132314","31132312","21114223","41114221","21123313","41123311","11114314","31114312","22141132","11241133","31241131","12132133","32132131","12141223","32141221","21241222","22123132","11223133","22132222","11232223","22141312","11241313","31241311","12114133","32114131","12123223","32123221","12132313","32132311","21232312","22114222","11214223","22123312","11223313","31223311","12114313","32114311","21214312","23141131","12241132","21341131","13132132","13141222","11332132","22241221","11341222","23123131","12223132","23132221","12232222","23141311","12241312","21341311","13114132","13123222","11314132","13132312","11323222","22232311","11332312","23114221","12214222","23123311","12223312","21323311","13114312","22214311","11314312","13241131","14132131","14141221","12332131","12341221","13223131","13232221","11423131","13241311","11432221","14114131","14123221","12314131","14132311","12323221","12332311","13214221","13223311","11414221","11423311","14114311","12314311","21151132","11142133","31142131","11151223","31151221","21133132","21142222","21151312","11124133","31124131","11133223","31133221","11142313","31142311","21115132","21124222","21133312","11115223","31115221","11124313","31124311","22151131","11251132","12142132","12151222","21251221","22133131","11233132","22142221","11242222","22151311","11251312","12124132","12133222","12142312","21242311","22115131","11215132","22124221","11224222","22133311","11233312","12115222","12124312","21224311","12251131","13142131","13151221","11342131","12233131","12242221","12251311","13124131","13133221","11324131","13142311","11333221","11342311","12215131","12224221","12233311","13115221","13124311","11315221","11324311","21161131","11152132","11161222","21143131","21152221","21161311","11134132","11143222","11152312","21125131","21134221","21143311","11116132","11125222","11134312","12152131","12161221","11243131","11252221","12134131","12143221","12152311","11225131","11234221","11243311","12116131","12125221","12134311","21111235","41111233","61111231","11111326","31111324","51111322","21111415","41111413","61111411","21211144","41211142","22111234","42111232","11211235","31211233","51211231","12111325","32111323","52111321","21211324","41211322","22111414","42111412","11211415","31211413","51211411","22211143","42211141","11311144","31311142","23111233","43111231","12211234","32211232","21311233","41311231","13111324","33111322","22211323","42211321","11311324","31311322","23111413","43111411","12211414","32211412","21311413","41311411","23211142","12311143","32311141","21411142","24111232","13211233","33211231","22311232","11411233","31411231","14111323","34111321","23211322","12311323","32311321","21411322","24111412","13211413","33211411","22311412","11411413","31411411","24211141","13311142","22411141","11511142","25111231","14211232","23311231","12411232","21511231","15111322","24211321","13311322","22411321","11511322","25111411","14211412","23311411","12411412","21511411","14311141","12511141","15211231","13411231","11611231","16111321","14311321","12511321","15211411","13411411","11611411","31121143","51121141","21112144","41112142","21121234","41121232","11112235","31112233","51112231","11121325","31121323","51121321","21112324","41112322","21121414","41121412","11112415","31112413","51112411","12121144","32121142","21221143","41221141","22112143","42112141","11212144","22121233","42121231","11221234","31221232","12112234","32112232","12121324","32121322","21221323","41221321","22112323","42112321","11212324","22121413","42121411","11221414","31221412","12112414","32112412","21212413","41212411","13121143","33121141","22221142","11321143","31321141","23112142","12212143","23121232","12221233","32221231","21321232","13112233","33112231","13121323","33121321","11312233","22221322","11321323","31321321","23112322","12212323","23121412","12221413","32221411","21321412","13112413","33112411","22212412","11312413","31312411","14121142","23221141","12321142","21421141","24112141","13212142","24121231","13221232","11412142","22321231","11421232","14112232","14121322","12312232","23221321","12321322","21421321","24112321","13212322","24121411","13221412","11412322","22321411","11421412","14112412","23212411","12312412","21412411","15121141","13321141","11521141","14212141","14221231","12412141","12421231","15112231","15121321","13312231","13321321","11512231","11521321","14212321","14221411","12412321","12421411","15112411","13312411","11512411","11131144","31131142","21122143","41122141","21131233","41131231","11113144","31113142","11122234","31122232","11131324","31131322","21113233","41113231","21122323","41122321","21131413","41131411","11113324","31113322","11122414","31122412","21113413","41113411","12131143","32131141","21231142","22122142","11222143","22131232","11231233","31231231","12113143","32113141","12122233","32122231","12131323","32131321","21231322","22113232","11213233","22122322","11222323","22131412","11231413","31231411","12113323","32113321","12122413","32122411","21222412","22113412","11213413","31213411","13131142","22231141","11331142","23122141","12222142","23131231","12231232","21331231","13113142","13122232","11313142","13131322","11322232","22231321","11331322","23113231","12213232","23122321","12222322","23131411","12231412","21331411","13113322","13122412","11313322","22222411","11322412","23113411","12213412","21313411","14131141","12331141","13222141","13231231","11422141","11431231","14113141","14122231","12313141","14131321","12322231","12331321","13213231","13222321","11413231","13231411","11422321","11431411","14113321","14122411","12313321","12322411","13213411","11413411","11141143","31141141","21132142","21141232","11123143","31123141","11132233","31132231","11141323","31141321","21114142","21123232","21132322","21141412","11114233","31114231","11123323","31123321","11132413","31132411","21114322","21123412","12141142","21241141","22132141","11232142","22141231","11241232","12123142","12132232","12141322","21241321","22114141","11214142","22123231","11223232","22132321","11232322","22141411","11241412","12114232","12123322","12132412","21232411","22114321","11214322","22123411","11223412","13141141","11341141","12232141","12241231","13123141","13132231","11323141","13141321","11332231","11341321","12214141","12223231","12232321","12241411","13114231","13123321","11314231","13132411","11323321","11332411","12214321","12223411","11151142","21142141","21151231","11133142","11142232","11151322","21124141","21133231","21142321","21151411","11115142","11124232","11133322","11142412","21115231","21124321","21133411","12151141","11242141","11251231","12133141","12142231","12151321","11224141","11233231","11242321","11251411","12115141","12124231","12133321","12142411","11215231","11224321","11233411","11161141","11143141","11152231","11161321","11125141","11134231","11143321","11152411","11111245","31111243","51111241","21111334","41111332","11111425","31111423","51111421","21111514","41111512","31211152","12111244","32111242","21211243","41211241","22111333","42111331","11211334","31211332","12111424","32111422","21211423","41211421","22111513","42111511","11211514","31211512","12211153","32211151","21311152","13111243","33111241","22211242","11311243","31311241","23111332","12211333","32211331","21311332","13111423","33111421","22211422","11311423","31311421","23111512","12211513","32211511","21311512","13211152","22311151","11411152","14111242","23211241","12311242","21411241","24111331","13211332","22311331","11411332","14111422","23211421","12311422","21411421","24111511","13211512","22311511","11411512","14211151","12411151","15111241","13311241","11511241","14211331","12411331","15111421","13311421","11511421","14211511","12411511","21121153","41121151","11112154","31112152","11121244","31121242","21112243","41112241","21121333","41121331","11112334","31112332","11121424","31121422","21112423","41112421","21121513","41121511","11112514","31112512","22121152","11221153","31221151","12112153","32112151","12121243","32121241","21221242","22112242","11212243","22121332","11221333","31221331","12112333","32112331","12121423","32121421","21221422","22112422","11212423","22121512","11221513","31221511","12112513","32112511","21212512","23121151","12221152","21321151","13112152","13121242","11312152","22221241","11321242","23112241","12212242","23121331","12221332","21321331","13112332","13121422","11312332","22221421","11321422","23112421","12212422","23121511","12221512","21321511","13112512","22212511","11312512","13221151","11421151","14112151","14121241","12312151","12321241","13212241","13221331","11412241","11421331","14112331","14121421","12312331","12321421","13212421","13221511","11412421","11421511","14112511","12312511","21131152","11122153","31122151","11131243","31131241","21113152","21122242","21131332","11113243","31113241","11122333","31122331","11131423","31131421","21113332","21122422","21131512","11113423","31113421","11122513","31122511","22131151","11231152","12122152","12131242","21231241","22113151","11213152","22122241","11222242","22131331","11231332","12113242","12122332","12131422","21231421","22113331","11213332","22122421","11222422","22131511","11231512","12113422","12122512","21222511","12231151","13122151","13131241","11322151","11331241","12213151","12222241","12231331","13113241","13122331","11313241","13131421","11322331","11331421","12213331","12222421","12231511","13113421","13122511","11313421","11322511","21141151","11132152","11141242","21123151","21132241","21141331","11114152","11123242","11132332","11141422","21114241","21123331","21132421","21141511","11114332","11123422","11132512","11241151","12132151","12141241","11223151","11232241","11241331","12114151","12123241","12132331","12141421","11214241","11223331","11232421","11241511","12114331","12123421","12132511","11142151","11151241","11124151","11133241","11142331","11151421","11115241","11124331","11133421","11142511","21111253","41111251","11111344","31111342","21111433","41111431","11111524","31111522","21111613","41111611","21211162","22111252","11211253","31211251","12111343","32111341","21211342","22111432","11211433","31211431","12111523","32111521","21211522","22111612","11211613","31211611","22211161","11311162","23111251","12211252","21311251","13111342","22211341","11311342","23111431","12211432","21311431","13111522","22211521","11311522","23111611","12211612","21311611","12311161","13211251","11411251","14111341","12311341","13211431","11411431","14111521","12311521","13211611","11411611","31121161","21112162","21121252","11112253","31112251","11121343","31121341","21112342","21121432","11112433","31112431","11121523","31121521","21112522","21121612","12121162","21221161","22112161","11212162","22121251","11221252","12112252","12121342","21221341","22112341","11212342","22121431","11221432","12112432","12121522","21221521","22112521","11212522","22121611","11221612","13121161","11321161","12212161","12221251","13112251","13121341","11312251","11321341","12212341","12221431","13112431","13121521","11312431","11321521","12212521","12221611","11131162","21122161","21131251","11113162"])]);$_.code49_parity=$a(["1001","0101","1100","0011","1010","0110","1111","0000"]);$_.charvals=new Map;for(var _T=0;_T<=48;_T+=1){$_.i=_T;var _V=$g($_.code49_charmap,$_.i);var _W=$t(_V);if($eq(_W,"stringtype")){_V=$g(_V,0)}$p($_.charvals,_V,$_.i)}for(var _c=0;_c<=127;_c+=1){$_.i=_c;var _e=$g($_.code49_combos,$_.i);$_.c1=$g(_e,0);$_.c2=$g(_e,1);if($_.c1==49){$p($_.charvals,$_.i,$a([$g($_.charvals,$_.code49_s1),$g($_.charvals,$_.c2)]))}if($_.c1==50){$p($_.charvals,$_.i,$a([$g($_.charvals,$_.code49_s2),$g($_.charvals,$_.c2)]))}}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_code49.globals);$_.dontdraw=false;$_.sam=-1;$_.append=false;$_.rows=0;$_.rowheight=8;$_.sepheight=1;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _10=$k[--$j];$_.options=_10;var _11=$k[--$j];$_.barcode=_11;if($_.barcode.length==0){$k[$j++]="bwipp.code49emptyData#20976";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.code49inputTooLong#20980";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.rows!=0&&($_.rows<2||$_.rows>8)){$k[$j++]="bwipp.code49badRows#20997";$k[$j++]="Code 49 must have 2 to 8 rows";bwipp_raiseerror()}if($_.rowheight<8||$_.rowheight>50){$k[$j++]="bwipp.code49badRowHeight#21001";$k[$j++]="Code 49 must have rowheight from 8 to 50";bwipp_raiseerror()}if($_.sepheight<1||$_.sepheight>5){$k[$j++]="bwipp.code49badSepHeight#21005";$k[$j++]="Code 49 must have sepheight from 1 to 5";bwipp_raiseerror()}if($_.append&&$_.sam!=-1){$k[$j++]="bwipp.code49samAndAppend#21009";$k[$j++]="sam and append cannot be specified together";bwipp_raiseerror()}if($_.sam!=-1){$k[$j++]=true;for(var _1G=0;_1G<1;_1G++){var _1H=$_.sam;if(_1H<12||_1H>99){$k[$j-1]=false;break}if($_.sam%10<2){$k[$j-1]=false;break}if(~~($_.sam/10)>$_.sam%10){$k[$j-1]=false;break}}var _1L=$k[--$j];if(!_1L){$k[$j++]="bwipp.code49badSAM#21020";$k[$j++]='SAM must be formatted as "NM" for Nth of M symbols, from 2 to 9 symbols';bwipp_raiseerror()}}$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.code49_fn1],["FNC2",$_.code49_fn2],["FNC3",$_.code49_fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _1Q=$k[--$j];$_.msg=_1Q;$_.msglen=$_.msg.length;$F($_.msg,function(){var _1T=$k[--$j];if(_1T>127){$k[$j++]="bwipp.code49badCharacter#21039";$k[$j++]="Code 49 can only support ASCII characters with values 0 to 127";bwipp_raiseerror()}});$_.encodealpha=function(){var _1V=$k[--$j];var _1W=$g($_.charvals,_1V);var _1X=$t(_1W);$k[$j++]=_1W;if($ne(_1X,"arraytype")){$as($a(1))}var _1Z=$k[--$j];$P($_.cws,$_.j,_1Z);$_.j=_1Z.length+$_.j};$_.base48=function(){var _1d=$k[--$j];$k[$j++]=0;$F(_1d,function(){var _1e=$k[--$j];var _1f=$k[--$j];$k[$j++]=$f(_1e-48+_1f*10)});$k[$j++]=Infinity;var _1g=$k[$j-2];var _1h=$k[$j-3];$k[$j-3]=$k[$j-1];$j-=2;$k[$j++]=_1g;for(var _1i=0,_1j=_1h-1;_1i<_1j;_1i++){var _1k=$k[--$j];$k[$j++]=_1k%48;$k[$j++]=~~(_1k/48)}var _1l=$a();$k[$j++]=Infinity;$k[$j++]=_1l;for(var _1o=_1l.length-1;_1o>=0;_1o-=1){var _1p=$k[--$j];$k[$j++]=$g(_1p,_1o);$k[$j++]=_1p}$j--;var _1r=$a();$P($_.cws,$_.j,_1r);$_.j=_1r.length+$_.j};$_.encodenumeric=function(){var _1v=$k[--$j];$_.nums=_1v;var _1x=$_.nums.length;var _1y=_1x%5;$k[$j++]="pre";$k[$j++]=_1x;$k[$j++]=_1y;if(_1y!=2){var _1z=$k[--$j];var _20=$k[--$j];$k[$j++]=$f(_20-_1z)}else{var _21=$k[--$j];var _22=$k[--$j];$k[$j++]=$f(_22-_21)-5}var _23=$k[--$j];var _24=$k[--$j];$_[_24]=_23;for(var _27=0,_26=$_.pre-1;_27<=_26;_27+=5){$k[$j++]=3;$k[$j++]=$G($_.nums,_27,5);$_.base48()}$_.nums=$G($_.nums,$_.pre,$f($_.nums.length-$_.pre));var _2G=$_.nums.length;$k[$j++]=_2G;if(_2G==1){$k[$j++]=$g($_.nums,$_.i);$_.encodealpha()}var _2K=$k[$j-1];if(_2K==3){$k[$j++]=2;$k[$j++]=$_.nums;$_.base48()}var _2M=$k[$j-1];if(_2M==4){$k[$j++]=3;$k[$j++]=Infinity;$k[$j++]=49;$k[$j++]=48;$q($_.nums);var _2O=$a();$k[$j++]=_2O;$_.base48()}var _2P=$k[--$j];if(_2P==7){$k[$j++]=3;$k[$j++]=Infinity;$k[$j++]=49;$k[$j++]=48;$q($G($_.nums,0,4));var _2S=$a();$k[$j++]=_2S;$_.base48();$k[$j++]=2;$k[$j++]=$G($_.nums,4,3);$_.base48()}};$k[$j++]=Infinity;for(var _2W=0,_2X=$_.msglen;_2W<_2X;_2W++){$k[$j++]=0}$k[$j++]=0;$_.numericruns=$a();for(var _2Z=$_.msglen-1;_2Z>=0;_2Z-=1){$_.i=_2Z;var _2c=$g($_.msg,$_.i);if(_2c>=48&&_2c<=57){$p($_.numericruns,$_.i,$g($_.numericruns,$_.i+1)+1)}else{$p($_.numericruns,$_.i,0)}}$_.cws=$a($_.msglen*2+1);for(;;){if($_.append){$_.mode=1;$_.method="alpha";$_.i=0;$_.j=0;break}if($_.sam!=-1){$_.mode=3;$k[$j++]=$_.cws;$k[$j++]=0;for(var _2p=0;_2p<=43;_2p+=1){$k[$j++]=_2p;if($g($_.code49_samval,_2p)!=$_.sam){$j--}}var _2s=$k[--$j];var _2t=$k[--$j];var _2u=$k[--$j];$p(_2u,_2t,_2s+1);$_.method="alpha";$_.i=0;$_.j=1;break}if($g($_.numericruns,0)>=5){$_.mode=2;$_.method="numeric";$_.i=0;$_.j=0;break}var _30=$g($_.charvals,$g($_.msg,0));var _31=$t(_30);$k[$j++]=_30;if($ne(_31,"arraytype")){$j--;$_.mode=0;$_.method="alpha";$_.i=0;$_.j=0;break}var _32=$k[--$j];var _34=$g(_32,0)==43?4:5;$_.mode=_34;$p($_.cws,0,$g($g($_.charvals,$g($_.msg,0)),1));$_.method="alpha";$_.i=1;$_.j=1;break}for(;;){if($_.i==$_.msglen){break}for(;;){if($eq($_.method,"alpha")){if($g($_.numericruns,$_.i)>=5){$k[$j++]=$_.code49_ns;$_.encodealpha();$_.method="numeric";break}$k[$j++]=$g($_.msg,$_.i);$_.encodealpha();$_.i=$_.i+1;break}if($eq($_.method,"numeric")){if($g($_.numericruns,$_.i)<5){$k[$j++]=$_.code49_ns;$_.encodealpha();$_.method="alpha";break}$k[$j++]=$G($_.msg,$_.i,$g($_.numericruns,$_.i));$_.encodenumeric();$_.i=$f($_.i+$g($_.numericruns,$_.i));break}}}$_.cws=$G($_.cws,0,$_.j);$_.urows=$_.rows;for(var _3d=0,_3e=$_.code49_metrics.length;_3d<_3e;_3d++){$_.m=$g($_.code49_metrics,_3d);$_.r=$g($_.m,0);$_.dcws=$g($_.m,1);$_.okay=true;if($_.urows!=0&&$_.urows!=$_.r){$_.okay=false}if($_.cws.length>$_.dcws){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.code49noValidSymbol#21172";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$k[$j++]=Infinity;$q($_.cws);for(var _3u=0,_3v=$f($_.dcws-$_.cws.length);_3u<_3v;_3u++){$k[$j++]=48}$_.cws=$a();$_.ccs=$a($_.r*8);$_.j=0;for(var _40=0,_3z=$_.r-2;_40<=_3z;_40+=1){$_.i=_40;$_.cc=$G($_.cws,$_.j,7);$P($_.ccs,$_.i*8,$_.cc);var _49=$_.cc;var _4G=$_.ccs;var _4F=$_.i*8+7;var _4E=0;for(var _4A=0,_4B=_49.length;_4A<_4B;_4A++){_4E=$f(_4E+$g(_49,_4A))}$p(_4G,_4F,_4E%49);$_.j=$_.j+7}if($_.j<$_.dcws){$P($_.ccs,$_.ccs.length-8,$G($_.cws,$_.j,$f($_.dcws-$_.j)))}$_.cr7=$f(($_.r-2)*7+$_.mode);$p($_.ccs,$_.ccs.length-2,$_.cr7);$_.calccheck=function(){var _4W=$k[--$j];$_.weights=_4W;$_.score=0;for(var _4Z=0,_4Y=~~(($_.r-1)*8/2)-1;_4Z<=_4Y;_4Z+=1){$_.i=_4Z;$_.score=$f(($g($_.ccs,$_.i*2)*49+$g($_.ccs,$_.i*2+1))*$g($_.weights,$_.i+1)+$_.score)}$k[$j++]=$_.score};$_.lastrow=$G($_.ccs,$_.ccs.length-8,8);if($_.r>=7){$k[$j++]=$_.cr7*$g($_.code49_weightz,0);$k[$j++]=$_.code49_weightz;$_.calccheck();var _4r=$k[--$j];var _4s=$k[--$j];var _4t=$f(_4s+_4r)%2401;var _4u=$a(2);$k[$j++]=~~(_4t/49);$k[$j++]=_4t%49;$as(_4u);$k[$j++]=$_.lastrow;$k[$j++]=0;var _4w=$k[$j-3];var _4x=$k[$j-1];var _4y=$k[$j-2];$j-=3;$P(_4y,_4x,_4w)}$_.wr1=$f($g($_.lastrow,0)*49+$g($_.lastrow,1));$k[$j++]=$_.cr7*$g($_.code49_weighty,0);$k[$j++]=$_.code49_weighty;$_.calccheck();var _55=$k[--$j];var _56=$k[--$j];var _5A=$f(_56+_55+$_.wr1*$g($_.code49_weighty,$_.r*4-3))%2401;$_.wr2=_5A;var _5B=$a(2);$k[$j++]=~~(_5A/49);$k[$j++]=_5A%49;$as(_5B);$k[$j++]=$_.lastrow;$k[$j++]=2;var _5D=$k[$j-3];var _5E=$k[$j-1];var _5F=$k[$j-2];$j-=3;$P(_5F,_5E,_5D);$k[$j++]=$_.cr7*$g($_.code49_weightx,0);$k[$j++]=$_.code49_weightx;$_.calccheck();var _5I=$k[--$j];var _5J=$k[--$j];var _5Q=$f(_5J+_5I+$_.wr1*$g($_.code49_weightx,$_.r*4-3)+$_.wr2*$g($_.code49_weightx,$_.r*4-2))%2401;var _5R=$a(2);$k[$j++]=~~(_5Q/49);$k[$j++]=_5Q%49;$as(_5R);$k[$j++]=$_.lastrow;$k[$j++]=4;var _5T=$k[$j-3];var _5U=$k[$j-1];var _5V=$k[$j-2];$j-=3;$P(_5V,_5U,_5T);var _5Y=$G($_.ccs,$_.ccs.length-8,7);var _5d=0;for(var _5Z=0,_5a=_5Y.length;_5Z<_5a;_5Z++){_5d=$f(_5d+$g(_5Y,_5Z))}$p($_.ccs,$_.ccs.length-1,_5d%49);if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#21254";$k[$j++]=$_.ccs;bwipp_raiseerror()}$_.rowbits=$a($_.r);for(var _5n=0,_5m=$_.r-1;_5n<=_5m;_5n+=1){$_.i=_5n;var _5s=$_.i!=$_.r-1?$g($_.code49_parity,$_.i):"0000";$_.p=_5s;$_.ccrow=$G($_.ccs,$_.i*8,8);$k[$j++]=Infinity;for(var _5x=0;_5x<=7;_5x+=2){$q($G($_.ccrow,_5x,2));var _60=$k[--$j];var _61=$k[--$j];$k[$j++]=$f(_60+_61*49)}$_.scrow=$a();$k[$j++]=Infinity;$k[$j++]=10;$k[$j++]=1;$k[$j++]=1;for(var _62=0;_62<=3;_62+=1){$_.j=_62;$F($g($g($_.code49_patterns,$g($_.p,$_.j)-48),$g($_.scrow,$_.j)),function(){$k[$j-1]-=48})}$k[$j++]=4;$k[$j++]=1;$_.sbs=$a();$k[$j++]=Infinity;var _6C=$_.sbs;$k[$j++]=1;for(var _6D=0,_6E=_6C.length;_6D<_6E;_6D++){var _6G=$k[$j-1];var _6H=_6G==0?1:0;for(var _6I=0,_6J=$g(_6C,_6D);_6I<_6J;_6I++){$k[$j++]=_6H}}$as($a($m()-1));var _6M=$k[--$j];$j--;$k[$j-1]=_6M;$k[$j++]=$_.rowbits;$k[$j++]=$_.i;var _6S=$k[$j-3];var _6T=$k[$j-1];var _6U=$k[$j-2];$j-=3;$p(_6U,_6T,_6S)}$_.numcomprows=$_.r*2+1;$k[$j++]=Infinity;for(var _6W=0;_6W<81;_6W++){$k[$j++]=1}for(var _6Z=0,_6Y=$_.r-2;_6Z<=_6Y;_6Z+=1){$_.i=_6Z;$q($g($_.rowbits,$_.i));for(var _6d=0;_6d<10;_6d++){$k[$j++]=0}for(var _6e=0;_6e<70;_6e++){$k[$j++]=1}$k[$j++]=0}$q($g($_.rowbits,$_.r-1));for(var _6i=0;_6i<81;_6i++){$k[$j++]=1}$_.pixs=$a();$_.symhgt=$f($_.sepheight*($_.r+1)+$_.r*$_.rowheight);$k[$j++]=Infinity;$k[$j++]=$_.sepheight;for(var _6q=0,_6p=$_.r-2;_6q<=_6p;_6q+=1){$k[$j++]=$_.rowheight;$k[$j++]=$_.sepheight}$k[$j++]=$_.rowheight;$k[$j++]=$_.sepheight;$_.rowmult=$a();$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",81],["pixy",$_.symhgt],["rowmult",$_.rowmult],["height",$_.symhgt/72],["width",81/72],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_flattermarken(){if(!bwipp_flattermarken.globals){var $__=$_;$_=bwipp_flattermarken.globals={};$_.flattermarken_encs=$a(["0018","0117","0216","0315","0414","0513","0612","0711","0810","0900"]);$_.flattermarken_barchars="1234567890";$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_flattermarken.globals);$_.dontdraw=false;$_.includetext=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;if($_.barcode.length>500){$k[$j++]="bwipp.flattermarkeninputTooLong#21708";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.flattermarkenBadCharacter#21721";$k[$j++]="Flattermarken must contain only digits";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;$_.sbs=$s($_.barlen*4);for(var _D=0,_C=$_.barlen-1;_D<=_C;_D+=1){$_.i=_D;$X($_.flattermarken_barchars,$G($_.barcode,$_.i,1));$j--;var _H=$k[--$j];$_.indx=_H.length;$j-=2;$_.enc=$g($_.flattermarken_encs,$_.indx);$P($_.sbs,$_.i*4,$_.enc)}if($_.height==-1){$_.height=.3}$k[$j++]=Infinity;var _S=[];var _O=$_.sbs;for(var _P=0,_Q=_O.length;_P<_Q;_P++){_S[_P]=$g(_O,_P)-48}_S=$a(_S);var _X=[];for(var _U=0,_V=~~(($_.sbs.length+1)/2);_U<_V;_U++){_X[_U]=$_.height}_X=$a(_X);var _b=[];for(var _Z=0,_a=~~(($_.sbs.length+1)/2);_Z<_a;_Z++){_b[_Z]=0}_b=$a(_b);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_S;$k[$j++]="bhs";$k[$j++]=_X;$k[$j++]="bbs";$k[$j++]=_b;$k[$j++]="txt";$k[$j++]=$a([$a([$_.barcode,0,0,$_.text1font,$_.text1size])]);$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="opt";$k[$j++]=$_.options;var _i=$d();$k[$j++]=_i;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_raw(){var $__=$_;$_=Object.assign({},$_,bwipp_raw.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length>2500){$k[$j++]="bwipp.rawinputTooLong#21830";$k[$j++]="The input data is too long";bwipp_raiseerror()}$F($_.barcode,function(){var _6=$k[--$j];if(_6<49||_6>57){$k[$j++]="bwipp.rawBadCharacter#21835";$k[$j++]="Raw must contain only digits 1 to 9";bwipp_raiseerror()}});if($_.height==-1){$_.height=1}$k[$j++]=Infinity;$k[$j++]=Infinity;$F($_.barcode,function(){$k[$j-1]-=48});var _A=$a();var _F=[];for(var _C=0,_D=~~(($_.barcode.length+1)/2);_C<_D;_C++){_F[_C]=$_.height}_F=$a(_F);var _J=[];for(var _H=0,_I=~~(($_.barcode.length+1)/2);_H<_I;_H++){_J[_H]=0}_J=$a(_J);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_A;$k[$j++]="bhs";$k[$j++]=_F;$k[$j++]="bbs";$k[$j++]=_J;$k[$j++]="opt";$k[$j++]=$_.options;var _L=$d();$k[$j++]=_L;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_daft(){var $__=$_;$_=Object.assign({},$_,bwipp_daft.globals);$_.dontdraw=false;$_.height=-1;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.daftEmptyData#21925";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>2500){$k[$j++]="bwipp.daftinputTooLong#21929";$k[$j++]="The input data is too long";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7!=68&&(_7!=65&&(_7!=70&&_7!=84))){$k[$j++]="bwipp.daftBadCharacter#21934";$k[$j++]="DAFT must contain only characters D, A, F and T";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;if($_.height==-1){$_.height=.175}$_.bbs=$a($_.barlen);$_.bhs=$a($_.barlen);for(var _G=0,_F=$_.barlen-1;_G<=_F;_G+=1){$_.i=_G;$_.enc=$G($_.barcode,$_.i,1);if($eq($_.enc,"D")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"A")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,5*$_.height/8)}if($eq($_.enc,"F")){$p($_.bbs,$_.i,0*$_.height/8);$p($_.bhs,$_.i,8*$_.height/8)}if($eq($_.enc,"T")){$p($_.bbs,$_.i,3*$_.height/8);$p($_.bhs,$_.i,2*$_.height/8)}}$k[$j++]=Infinity;var _m=$_.bbs;var _n=$_.bhs;$k[$j++]=Infinity;for(var _p=0,_q=$_.bhs.length-1;_p<_q;_p++){$k[$j++]=1.44;$k[$j++]=1.872}$k[$j++]=1.44;var _r=$a();$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="bbs";$k[$j++]=_m;$k[$j++]="bhs";$k[$j++]=_n;$k[$j++]="sbs";$k[$j++]=_r;$k[$j++]="opt";$k[$j++]=$_.options;var _t=$d();$k[$j++]=_t;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_symbol(){if(!bwipp_symbol.globals){var $__=$_;$_=bwipp_symbol.globals={};$_.symbol_fimasbs=$a([2.25,2.25,2.25,11.25,2.25,11.25,2.25,2.25,2.25]);var _3=[];for(var _1=0,_2=~~($_.symbol_fimasbs.length/2)+1;_1<_2;_1++){_3[_1]=.625}_3=$a(_3);$_.symbol_fimabhs=_3;var _6=[];for(var _4=0,_5=~~($_.symbol_fimasbs.length/2)+1;_4<_5;_4++){_6[_4]=0}_6=$a(_6);$_.symbol_fimabbs=_6;$_.symbol_fimbsbs=$a([2.25,6.75,2.25,2.25,2.25,6.25,2.25,2.25,2.25,6.75,2.25]);var _A=[];for(var _8=0,_9=~~($_.symbol_fimbsbs.length/2)+1;_8<_9;_8++){_A[_8]=.625}_A=$a(_A);$_.symbol_fimbbhs=_A;var _D=[];for(var _B=0,_C=~~($_.symbol_fimbsbs.length/2)+1;_B<_C;_B++){_D[_B]=0}_D=$a(_D);$_.symbol_fimbbbs=_D;$_.symbol_fimcsbs=$a([2.25,2.25,2.25,6.75,2.25,6.75,2.25,6.75,2.25,2.25,2.25]);var _H=[];for(var _F=0,_G=~~($_.symbol_fimcsbs.length/2)+1;_F<_G;_F++){_H[_F]=.625}_H=$a(_H);$_.symbol_fimcbhs=_H;var _K=[];for(var _I=0,_J=~~($_.symbol_fimcsbs.length/2)+1;_I<_J;_I++){_K[_I]=0}_K=$a(_K);$_.symbol_fimcbbs=_K;$_.symbol_fimdsbs=$a([2.25,2.25,2.25,2.25,2.25,6.75,2.25,6.75,2.25,2.25,2.25,2.25,2.25]);var _O=[];for(var _M=0,_N=~~($_.symbol_fimdsbs.length/2)+1;_M<_N;_M++){_O[_M]=.625}_O=$a(_O);$_.symbol_fimdbhs=_O;var _R=[];for(var _P=0,_Q=~~($_.symbol_fimdsbs.length/2)+1;_P<_Q;_P++){_R[_P]=0}_R=$a(_R);$_.symbol_fimdbbs=_R;$_.symbol_fimesbs=$a([2.25,6.75,2.25,15.75,2.25,6.75,2.25]);var _V=[];for(var _T=0,_U=~~($_.symbol_fimesbs.length/2)+1;_T<_U;_T++){_V[_T]=.625}_V=$a(_V);$_.symbol_fimebhs=_V;var _Y=[];for(var _W=0,_X=~~($_.symbol_fimesbs.length/2)+1;_W<_X;_W++){_Y[_W]=0}_Y=$a(_Y);$_.symbol_fimebbs=_Y;$k[$j++]=Infinity;for(var _Z=0;_Z<100;_Z++){$k[$j++]=1;$k[$j++]=1}$k[$j++]=1;var _a=$a();$_.symbol_zebrasbs=_a;var _d=[];for(var _b=0,_c=~~($_.symbol_zebrasbs.length/2)+1;_b<_c;_b++){_d[_b]=1}_d=$a(_d);$_.symbol_zebrabhs=_d;var _g=[];for(var _e=0,_f=~~($_.symbol_zebrasbs.length/2)+1;_e<_f;_e++){_g[_e]=0}_g=$a(_g);$_.symbol_zebrabbs=_g;$_.symbol_checksize=199;$k[$j++]=Infinity;var _h=$_.symbol_checksize;for(var _i=0,_j=~~(_h*_h/2);_i<_j;_i++){$k[$j++]=1;$k[$j++]=0}$k[$j++]=1;var _k=$a();$_.symbol_checkpixs=_k;var _s=new Map([["fima",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_fimasbs],["bhs",$_.symbol_fimabhs],["bbs",$_.symbol_fimabbs]])}],["fimb",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_fimbsbs],["bhs",$_.symbol_fimbbhs],["bbs",$_.symbol_fimbbbs]])}],["fimc",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_fimcsbs],["bhs",$_.symbol_fimcbhs],["bbs",$_.symbol_fimcbbs]])}],["fimd",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_fimdsbs],["bhs",$_.symbol_fimdbhs],["bbs",$_.symbol_fimdbbs]])}],["fime",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_fimesbs],["bhs",$_.symbol_fimebhs],["bbs",$_.symbol_fimebbs]])}],["zebra",function(){$k[$j++]=new Map([["ren","renlinear"],["sbs",$_.symbol_zebrasbs],["bhs",$_.symbol_zebrabhs],["bbs",$_.symbol_zebrabbs]])}],["check",function(){$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.symbol_checkpixs],["pixx",$_.symbol_checksize],["pixy",$_.symbol_checksize],["height",$_.symbol_checksize/72],["width",$_.symbol_checksize/72]])}]]);$_.symbol_symbols=_s;var _t=new Map([["renlinear",bwipp_renlinear],["renmatrix",bwipp_renmatrix]]);$_.symbol_renmap=_t;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_symbol.globals);$_.dontdraw=false;$_.width=0;$_.modunit=1;bwipp_processoptions();var _u=$k[--$j];$_.options=_u;var _v=$k[--$j];$_.barcode=_v;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length>50){$k[$j++]="bwipp.symbolinputTooLong#22096";$k[$j++]="The input data is too long";bwipp_raiseerror()}if(!$has($_.symbol_symbols,$_.barcode)){$k[$j++]="bwipp.symbolUnknownSymbol#22100";$k[$j++]="Unknown symbol name provided";bwipp_raiseerror()}if($g($_.symbol_symbols,$_.barcode)()===true){return true}var _13=$k[$j-1];$p(_13,"strictspec",$_.strictspec);$p(_13,"loosespec",$_.loosespec);$p(_13,"xdim",$_.xdim);$p(_13,"xmin",$_.xmin);$p(_13,"xmax",$_.xmax);$p(_13,"modunit",$_.modunit);$p(_13,"opt",$_.options);if($_._render){var _1C=$k[$j-1];var _1E=$g($_.symbol_renmap,$g(_1C,"ren"));if(_1E()===true){return true}}$_=$__}function bwipp_pdf417(){if(!bwipp_pdf417.globals){var $__=$_;$_=bwipp_pdf417.globals={};$_.pdf417_t=0;$_.pdf417_n=1;$_.pdf417_b=2;$_.pdf417_a=0;$_.pdf417_l=1;$_.pdf417_m=2;$_.pdf417_p=3;$_.pdf417_tl=-1;$_.pdf417_nl=-2;$_.pdf417_bl=-3;$_.pdf417_bl6=-4;$_.pdf417_bs=-5;$_.pdf417_al=-6;$_.pdf417_ll=-7;$_.pdf417_ml=-8;$_.pdf417_pl=-9;$_.pdf417_as=-10;$_.pdf417_ps=-11;$_.al=$_.pdf417_al;$_.ll=$_.pdf417_ll;$_.ml=$_.pdf417_ml;$_.pl=$_.pdf417_pl;$_.as=$_.pdf417_as;$_.ps=$_.pdf417_ps;$_.pdf417_textsubmodes=$a([$_.pdf417_a,$_.pdf417_l,$_.pdf417_m,$_.pdf417_p]);$_.pdf417_charmaps=$a([$a(["A","a","0",";"]),$a(["B","b","1","<"]),$a(["C","c","2",">"]),$a(["D","d","3","@"]),$a(["E","e","4","["]),$a(["F","f","5",92]),$a(["G","g","6","]"]),$a(["H","h","7","_"]),$a(["I","i","8","`"]),$a(["J","j","9","~"]),$a(["K","k","&","!"]),$a(["L","l",13,13]),$a(["M","m",9,9]),$a(["N","n",",",","]),$a(["O","o",":",":"]),$a(["P","p","#",10]),$a(["Q","q","-","-"]),$a(["R","r",".","."]),$a(["S","s","$","$"]),$a(["T","t","/","/"]),$a(["U","u","+",'"']),$a(["V","v","%","|"]),$a(["W","w","*","*"]),$a(["X","x","=",40]),$a(["Y","y","^",41]),$a(["Z","z",$_.pl,"?"]),$a([" "," "," ","{"]),$a([$_.ll,$_.as,$_.ll,"}"]),$a([$_.ml,$_.ml,$_.al,"'"]),$a([$_.ps,$_.ps,$_.ps,$_.al])]);$_.pdf417_e=4e4;$_.e=$_.pdf417_e;$_.pdf417_latlen=$a([$a([0,1,1,2]),$a([2,0,1,2]),$a([1,1,0,1]),$a([1,2,2,0])]);$_.pdf417_latseq=$a([$a([$a([]),$a([$_.ll]),$a([$_.ml]),$a([$_.ml,$_.pl])]),$a([$a([$_.ml,$_.al]),$a([]),$a([$_.ml]),$a([$_.ml,$_.pl])]),$a([$a([$_.al]),$a([$_.ll]),$a([]),$a([$_.pl])]),$a([$a([$_.al]),$a([$_.al,$_.ll]),$a([$_.al,$_.ml]),$a([])])]);$_.pdf417_shftlen=$a([$a([$_.e,$_.e,$_.e,1]),$a([1,$_.e,$_.e,1]),$a([$_.e,$_.e,$_.e,1]),$a([$_.e,$_.e,$_.e,$_.e])]);$k[$j++]=Infinity;$k[$j++]=$_.pdf417_tl;$k[$j++]=900;$k[$j++]=$_.pdf417_bl;$k[$j++]=901;$k[$j++]=$_.pdf417_bl6;$k[$j++]=924;$k[$j++]=$_.pdf417_nl;$k[$j++]=902;$k[$j++]=$_.pdf417_bs;$k[$j++]=913;var _1j=$d();$_.pdf417_latchcws=_1j;$_.pdf417_textencfuncs=$a(["enca","encl","encm","encp"]);$_.pdf417_encfuncs=$a(["enct","encn","encb"]);$_.pdf417_clusters=$a([$a([120256,125680,128380,120032,125560,128318,108736,119920,108640,86080,108592,86048,110016,120560,125820,109792,120440,125758,88256,109680,88160,89536,110320,120700,89312,110200,120638,89200,110140,89840,110460,89720,110398,89980,128506,119520,125304,128190,107712,119408,125244,107616,119352,84032,107568,119324,84e3,107544,83984,108256,119672,125374,85184,108144,119612,85088,108088,119582,85040,108060,85728,108408,119742,85616,108348,85560,108318,85880,108478,85820,85790,107200,119152,125116,107104,119096,125086,83008,107056,119068,82976,107032,82960,82952,83648,107376,119228,83552,107320,119198,83504,107292,83480,83468,83824,107452,83768,107422,83740,83900,106848,118968,125022,82496,106800,118940,82464,106776,118926,82448,106764,82440,106758,82784,106936,119006,82736,106908,82712,106894,82700,82694,106974,82830,82240,106672,118876,82208,106648,118862,82192,106636,82184,106630,82180,82352,82328,82316,82080,118830,106572,106566,82050,117472,124280,127678,103616,117360,124220,103520,117304,124190,75840,103472,75808,104160,117624,124350,76992,104048,117564,76896,103992,76848,76824,77536,104312,117694,77424,104252,77368,77340,77688,104382,77628,77758,121536,126320,128700,121440,126264,128670,111680,121392,126236,111648,121368,126222,111632,121356,103104,117104,124092,112320,103008,117048,124062,112224,121656,126366,93248,74784,102936,117006,93216,112152,93200,75456,103280,117180,93888,75360,103224,117150,93792,112440,121758,93744,75288,93720,75632,103356,94064,75576,103326,94008,112542,93980,75708,94140,75678,94110,121184,126136,128606,111168,121136,126108,111136,121112,126094,111120,121100,111112,111108,102752,116920,123998,111456,102704,116892,91712,74272,121244,116878,91680,74256,102668,91664,111372,102662,74244,74592,102840,116958,92e3,74544,102812,91952,111516,102798,91928,74508,74502,74680,102878,92088,74652,92060,74638,92046,92126,110912,121008,126044,110880,120984,126030,110864,120972,110856,120966,110852,110850,74048,102576,116828,90944,74016,102552,116814,90912,111e3,121038,90896,73992,102534,90888,110982,90884,74160,102620,91056,74136,102606,91032,111054,91020,74118,91014,91100,91086,110752,120920,125998,110736,120908,110728,120902,110724,110722,73888,102488,116782,90528,73872,102476,90512,110796,102470,90504,73860,90500,73858,73944,90584,90572,90566,120876,120870,110658,102444,73800,90312,90308,90306,101056,116080,123580,100960,116024,70720,100912,115996,70688,100888,70672,70664,71360,101232,116156,71264,101176,116126,71216,101148,71192,71180,71536,101308,71480,101278,71452,71612,71582,118112,124600,127838,105024,118064,124572,104992,118040,124558,104976,118028,104968,118022,100704,115896,123486,105312,100656,115868,79424,70176,118172,115854,79392,105240,100620,79376,70152,79368,70496,100792,115934,79712,70448,118238,79664,105372,100750,79640,70412,79628,70584,100830,79800,70556,79772,70542,70622,79838,122176,126640,128860,122144,126616,128846,122128,126604,122120,126598,122116,104768,117936,124508,113472,104736,126684,124494,113440,122264,126670,113424,104712,117894,113416,122246,104706,69952,100528,115804,78656,69920,100504,115790,96064,78624,104856,117966,96032,113560,122318,100486,96016,78600,104838,96008,69890,70064,100572,78768,70040,100558,96176,78744,104910,96152,113614,70022,78726,70108,78812,70094,96220,78798,122016,126552,128814,122e3,126540,121992,126534,121988,121986,104608,117848,124462,113056,104592,126574,113040,122060,117830,113032,104580,113028,104578,113026,69792,100440,115758,78240,69776,100428,95136,78224,104652,100422,95120,113100,69764,95112,78212,69762,78210,69848,100462,78296,69836,95192,78284,69830,95180,78278,69870,95214,121936,126508,121928,126502,121924,121922,104528,117804,112848,104520,117798,112840,121958,112836,104514,112834,69712,100396,78032,69704,100390,94672,78024,104550,94664,112870,69698,94660,78018,94658,78060,94700,94694,126486,121890,117782,104484,104482,69672,77928,94440,69666,77922,99680,68160,99632,68128,99608,115342,68112,99596,68104,99590,68448,99768,115422,68400,99740,68376,99726,68364,68358,68536,99806,68508,68494,68574,101696,116400,123740,101664,116376,101648,116364,101640,116358,101636,67904,99504,115292,72512,67872,116444,115278,72480,101784,116430,72464,67848,99462,72456,101766,67842,68016,99548,72624,67992,99534,72600,101838,72588,67974,68060,72668,68046,72654,118432,124760,127918,118416,124748,118408,124742,118404,118402,101536,116312,105888,101520,116300,105872,118476,116294,105864,101508,105860,101506,105858,67744,99416,72096,67728,116334,80800,72080,101580,99398,80784,105932,67716,80776,72068,67714,72066,67800,99438,72152,67788,80856,72140,67782,80844,72134,67822,72174,80878,126800,128940,126792,128934,126788,126786,118352,124716,122576,126828,124710,122568,126822,122564,118338,122562,101456,116268,105680,101448,116262,114128,105672,118374,114120,122598,101442,114116,105666,114114,67664,99372,71888,67656,99366,80336,71880,101478,97232,80328,105702,67650,97224,114150,71874,97220,67692,71916,67686,80364,71910,97260,80358,97254,126760,128918,126756,126754,118312,124694,122472,126774,122468,118306,122466,101416,116246,105576,101412,113896,105572,101410,113892,105570,113890,67624,99350,71784,101430,80104,71780,67618,96744,80100,71778,96740,80098,96738,71798,96758,126738,122420,122418,105524,113780,113778,71732,79988,96500,96498,66880,66848,98968,66832,66824,66820,66992,66968,66956,66950,67036,67022,1e5,99984,115532,99976,115526,99972,99970,66720,98904,69024,100056,98892,69008,100044,69e3,100038,68996,66690,68994,66776,98926,69080,100078,69068,66758,69062,66798,69102,116560,116552,116548,116546,99920,102096,116588,115494,102088,116582,102084,99906,102082,66640,68816,66632,98854,73168,68808,66628,73160,68804,66626,73156,68802,66668,68844,66662,73196,68838,73190,124840,124836,124834,116520,118632,124854,118628,116514,118626,99880,115478,101992,116534,106216,101988,99874,106212,101986,106210,66600,98838,68712,99894,72936,68708,66594,81384,72932,68706,81380,72930,66614,68726,72950,81398,128980,128978,124820,126900,124818,126898,116500,118580,116498,122740,118578,122738,99860,101940,99858,106100,101938,114420]),$a([128352,129720,125504,128304,129692,125472,128280,129678,125456,128268,125448,128262,125444,125792,128440,129758,120384,125744,128412,120352,125720,128398,120336,125708,120328,125702,120324,120672,125880,128478,110144,120624,125852,110112,120600,125838,110096,120588,110088,120582,110084,110432,120760,125918,89664,110384,120732,89632,110360,120718,89616,110348,89608,110342,89952,110520,120798,89904,110492,89880,110478,89868,90040,110558,90012,89998,125248,128176,129628,125216,128152,129614,125200,128140,125192,128134,125188,125186,119616,125360,128220,119584,125336,128206,119568,125324,119560,125318,119556,119554,108352,119728,125404,108320,119704,125390,108304,119692,108296,119686,108292,108290,85824,108464,119772,85792,108440,119758,85776,108428,85768,108422,85764,85936,108508,85912,108494,85900,85894,85980,85966,125088,128088,129582,125072,128076,125064,128070,125060,125058,119200,125144,128110,119184,125132,119176,125126,119172,119170,107424,119256,125166,107408,119244,107400,119238,107396,107394,83872,107480,119278,83856,107468,83848,107462,83844,83842,83928,107502,83916,83910,83950,125008,128044,125e3,128038,124996,124994,118992,125036,118984,125030,118980,118978,106960,119020,106952,119014,106948,106946,82896,106988,82888,106982,82884,82882,82924,82918,124968,128022,124964,124962,118888,124982,118884,118882,106728,118902,106724,106722,82408,106742,82404,82402,124948,124946,118836,118834,106612,106610,124224,127664,129372,124192,127640,129358,124176,127628,124168,127622,124164,124162,117568,124336,127708,117536,124312,127694,117520,124300,117512,124294,117508,117506,104256,117680,124380,104224,117656,124366,104208,117644,104200,117638,104196,104194,77632,104368,117724,77600,104344,117710,77584,104332,77576,104326,77572,77744,104412,77720,104398,77708,77702,77788,77774,128672,129880,93168,128656,129868,92664,128648,129862,92412,128644,128642,124064,127576,129326,126368,124048,129902,126352,128716,127558,126344,124036,126340,124034,126338,117152,124120,127598,121760,117136,124108,121744,126412,124102,121736,117124,121732,117122,121730,103328,117208,124142,112544,103312,117196,112528,121804,117190,112520,103300,112516,103298,112514,75680,103384,117230,94112,75664,103372,94096,112588,103366,94088,75652,94084,75650,75736,103406,94168,75724,94156,75718,94150,75758,128592,129836,91640,128584,129830,91388,128580,91262,128578,123984,127532,126160,123976,127526,126152,128614,126148,123970,126146,116944,124012,121296,116936,124006,121288,126182,121284,116930,121282,102864,116972,111568,102856,116966,111560,121318,111556,102850,111554,74704,102892,92112,74696,102886,92104,111590,92100,74690,92098,74732,92140,74726,92134,128552,129814,90876,128548,90750,128546,123944,127510,126056,128566,126052,123938,126050,116840,123958,121064,116836,121060,116834,121058,102632,116854,111080,121078,111076,102626,111074,74216,102646,91112,74212,91108,74210,91106,74230,91126,128532,90494,128530,123924,126004,123922,126002,116788,120948,116786,120946,102516,110836,102514,110834,73972,90612,73970,90610,128522,123914,125978,116762,120890,102458,110714,123552,127320,129198,123536,127308,123528,127302,123524,123522,116128,123608,127342,116112,123596,116104,123590,116100,116098,101280,116184,123630,101264,116172,101256,116166,101252,101250,71584,101336,116206,71568,101324,71560,101318,71556,71554,71640,101358,71628,71622,71662,127824,129452,79352,127816,129446,79100,127812,78974,127810,123472,127276,124624,123464,127270,124616,127846,124612,123458,124610,115920,123500,118224,115912,123494,118216,124646,118212,115906,118210,100816,115948,105424,100808,115942,105416,118246,105412,100802,105410,70608,100844,79824,70600,100838,79816,105446,79812,70594,79810,70636,79852,70630,79846,129960,95728,113404,129956,95480,113278,129954,95356,95294,127784,129430,78588,128872,129974,95996,78462,128868,127778,95870,128866,123432,127254,124520,123428,126696,128886,123426,126692,124514,126690,115816,123446,117992,115812,122344,117988,115810,122340,117986,122338,100584,115830,104936,100580,113640,104932,100578,113636,104930,113634,70120,100598,78824,70116,96232,78820,70114,96228,78818,96226,70134,78838,129940,94968,113022,129938,94844,94782,127764,78206,128820,127762,95102,128818,123412,124468,123410,126580,124466,126578,115764,117876,115762,122100,117874,122098,100468,104692,100466,113140,104690,113138,69876,78324,69874,95220,78322,95218,129930,94588,94526,127754,128794,123402,124442,126522,115738,117818,121978,100410,104570,112890,69754,78074,94714,94398,123216,127148,123208,127142,123204,123202,115408,123244,115400,123238,115396,115394,99792,115436,99784,115430,99780,99778,68560,99820,68552,99814,68548,68546,68588,68582,127400,129238,72444,127396,72318,127394,123176,127126,123752,123172,123748,123170,123746,115304,123190,116456,115300,116452,115298,116450,99560,115318,101864,99556,101860,99554,101858,68072,99574,72680,68068,72676,68066,72674,68086,72694,129492,80632,105854,129490,80508,80446,127380,72062,127924,127378,80766,127922,123156,123700,123154,124788,123698,124786,115252,116340,115250,118516,116338,118514,99444,101620,99442,105972,101618,105970,67828,72180,67826,80884,72178,80882,97008,114044,96888,113982,96828,96798,129482,80252,130010,97148,80190,97086,127370,127898,128954,123146,123674,124730,126842,115226,116282,118394,122618,99386,101498,105722,114170,67706,71930,80378,96632,113854,96572,96542,80062,96702,96444,96414,96350,123048,123044,123042,115048,123062,115044,115042,99048,115062,99044,99042,67048,99062,67044,67042,67062,127188,68990,127186,123028,123316,123026,123314,114996,115572,114994,115570,98932,100084,98930,100082,66804,69108,66802,69106,129258,73084,73022,127178,127450,123018,123290,123834,114970,115514,116602,98874,99962,102138,66682,68858,73210,81272,106174,81212,81182,72894,81342,97648,114364,97592,114334,97564,97550,81084,97724,81054,97694,97464,114270,97436,97422,80990,97502,97372,97358,97326,114868,114866,98676,98674,66292,66290,123098,114842,115130,98618,99194,66170,67322,69310,73404,73374,81592,106334,81564,81550,73310,81630,97968,114524,97944,114510,97932,97926,81500,98012,81486,97998,97880,114478,97868,97862,81454,97902,97836,97830,69470,73564,73550,81752,106414,81740,81734,73518,81774,81708,81702]),$a([109536,120312,86976,109040,120060,86496,108792,119934,86256,108668,86136,129744,89056,110072,129736,88560,109820,129732,88312,109694,129730,88188,128464,129772,89592,128456,129766,89340,128452,89214,128450,125904,128492,125896,128486,125892,125890,120784,125932,120776,125926,120772,120770,110544,120812,110536,120806,110532,84928,108016,119548,84448,107768,119422,84208,107644,84088,107582,84028,129640,85488,108284,129636,85240,108158,129634,85116,85054,128232,129654,85756,128228,85630,128226,125416,128246,125412,125410,119784,125430,119780,119778,108520,119798,108516,108514,83424,107256,119166,83184,107132,83064,107070,83004,82974,129588,83704,107390,129586,83580,83518,128116,83838,128114,125172,125170,119284,119282,107508,107506,82672,106876,82552,106814,82492,82462,129562,82812,82750,128058,125050,119034,82296,106686,82236,82206,82366,82108,82078,76736,103920,117500,76256,103672,117374,76016,103548,75896,103486,75836,129384,77296,104188,129380,77048,104062,129378,76924,76862,127720,129398,77564,127716,77438,127714,124392,127734,124388,124386,117736,124406,117732,117730,104424,117750,104420,104418,112096,121592,126334,92608,111856,121468,92384,111736,121406,92272,111676,92216,111646,92188,75232,103160,117118,93664,74992,103036,93424,112252,102974,93304,74812,93244,74782,93214,129332,75512,103294,129908,129330,93944,75388,129906,93820,75326,93758,127604,75646,128756,127602,94078,128754,124148,126452,124146,126450,117236,121844,117234,121842,103412,103410,91584,111344,121212,91360,111224,121150,91248,111164,91192,111134,91164,91150,74480,102780,91888,74360,102718,91768,111422,91708,74270,91678,129306,74620,129850,92028,74558,91966,127546,128634,124026,126202,116986,121338,102906,90848,110968,121022,90736,110908,90680,110878,90652,90638,74104,102590,91e3,74044,90940,74014,90910,74174,91070,90480,110780,90424,110750,90396,90382,73916,90556,73886,90526,90296,110686,90268,90254,73822,90334,90204,90190,71136,101112,116094,70896,100988,70776,100926,70716,70686,129204,71416,101246,129202,71292,71230,127348,71550,127346,123636,123634,116212,116210,101364,101362,79296,105200,118140,79072,105080,118078,78960,105020,78904,104990,78876,78862,70384,100732,79600,70264,100670,79480,105278,79420,70174,79390,129178,70524,129466,79740,70462,79678,127290,127866,123514,124666,115962,118266,100858,113376,122232,126654,95424,113264,122172,95328,113208,122142,95280,113180,95256,113166,95244,78560,104824,117950,95968,78448,104764,95856,113468,104734,95800,78364,95772,78350,95758,70008,100542,78712,69948,96120,78652,69918,96060,78622,96030,70078,78782,96190,94912,113008,122044,94816,112952,122014,94768,112924,94744,112910,94732,94726,78192,104636,95088,78136,104606,95032,113054,95004,78094,94990,69820,78268,69790,95164,78238,95134,94560,112824,121950,94512,112796,94488,112782,94476,94470,78008,104542,94648,77980,94620,77966,94606,69726,78046,94686,94384,112732,94360,112718,94348,94342,77916,94428,77902,94414,94296,112686,94284,94278,77870,94318,94252,94246,68336,99708,68216,99646,68156,68126,68476,68414,127162,123258,115450,99834,72416,101752,116414,72304,101692,72248,101662,72220,72206,67960,99518,72568,67900,72508,67870,72478,68030,72638,80576,105840,118460,80480,105784,118430,80432,105756,80408,105742,80396,80390,72048,101564,80752,71992,101534,80696,71964,80668,71950,80654,67772,72124,67742,80828,72094,80798,114016,122552,126814,96832,113968,122524,96800,113944,122510,96784,113932,96776,113926,96772,80224,105656,118366,97120,80176,105628,97072,114076,105614,97048,80140,97036,80134,97030,71864,101470,80312,71836,97208,80284,71822,97180,80270,97166,67678,71902,80350,97246,96576,113840,122460,96544,113816,122446,96528,113804,96520,113798,96516,96514,80048,105564,96688,80024,105550,96664,113870,96652,80006,96646,71772,80092,71758,96732,80078,96718,96416,113752,122414,96400,113740,96392,113734,96388,96386,79960,105518,96472,79948,96460,79942,96454,71726,79982,96494,96336,113708,96328,113702,96324,96322,79916,96364,79910,96358,96296,113686,96292,96290,79894,96310,66936,99006,66876,66846,67006,68976,100028,68920,99998,68892,68878,66748,69052,66718,69022,73056,102072,116574,73008,102044,72984,102030,72972,72966,68792,99934,73144,68764,73116,68750,73102,66654,68830,73182,81216,106160,118620,81184,106136,118606,81168,106124,81160,106118,81156,81154,72880,101980,81328,72856,101966,81304,106190,81292,72838,81286,68700,72924,68686,81372,72910,81358,114336,122712,126894,114320,122700,114312,122694,114308,114306,81056,106072,118574,97696,81040,106060,97680,114380,106054,97672,81028,97668,81026,97666,72792,101934,81112,72780,97752,81100,72774,97740,81094,97734,68654,72814,81134,97774,114256,122668,114248,122662,114244,114242,80976,106028,97488,80968,106022,97480,114278,97476,80962,97474,72748,81004,72742,97516,80998,97510,114216,122646,114212,114210,80936,106006,97384,80932,97380,80930,97378,72726,80950,97398,114196,114194,80916,97332,80914,97330,66236,66206,67256,99166,67228,67214,66142,67294,69296,100188,69272,100174,69260,69254,67164,69340,67150,69326,73376,102232,116654,73360,102220,73352,102214,73348,73346,69208,100142,73432,102254,73420,69190,73414,67118,69230,73454,106320,118700,106312,118694,106308,106306,73296,102188,81616,106348,102182,81608,73284,81604,73282,81602,69164,73324,69158,81644,73318,81638,122792,126934,122788,122786,106280,118678,114536,106276,114532,106274,114530,73256,102166,81512,73252,98024,81508,73250,98020,81506,98018,69142,73270,81526,98038,122772,122770,106260,114484,106258,114482,73236,81460,73234,97908,81458,97906,122762,106250,114458,73226,81434,97850,66396,66382,67416,99246,67404,67398,66350,67438,69456,100268,69448,100262,69444,69442,67372,69484,67366,69478,102312,116694,102308,102306,69416,100246,73576,102326,73572,69410,73570,67350,69430,73590,118740,118738,102292,106420,102290,106418,69396,73524,69394,81780,73522,81778,118730,102282,106394,69386,73498,81722,66476,66470,67496,99286,67492,67490,66454,67510,100308,100306,67476,69556,67474,69554,116714])]);$_.pdf417_rsprod=function(){var _1q=$k[$j-1];var _1r=$k[$j-2];if(_1q!=0&&_1r!=0){var _1t=$k[--$j];var _1v=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_1t)+$g($_.rslog,_1v))%928)}else{$j-=2;$k[$j++]=0}};$_.pdf417_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _21=0,_22=$_.k;_21<_22;_21++){$k[$j++]=0}$_.coeffs=$a();for(var _25=1,_24=$_.k;_25<=_24;_25+=1){var _26=$_.coeffs;$p(_26,_25,$g(_26,_25-1));$k[$j++]=$g($_.rsalog,_25);for(var _2A=_25-1;_2A>=1;_2A-=1){var _2B=$_.coeffs;var _2D=$k[$j-1];$k[$j++]=_2B;$k[$j++]=_2A;$k[$j++]=$g(_2B,_2A);$k[$j++]=_2D;$_.pdf417_rsprod();var _2F=$k[--$j];var _2G=$k[--$j];var _2I=$k[--$j];$p(_2I,_2G,$f(_2F+$g($_.coeffs,_2G-1))%929)}var _2J=$_.coeffs;var _2L=$k[$j-1];$k[$j++]=_2J;$k[$j++]=0;$k[$j++]=$g(_2J,0);$k[$j++]=_2L;$_.pdf417_rsprod();var _2M=$k[--$j];var _2N=$k[--$j];var _2O=$k[--$j];$p(_2O,_2N,_2M);$j--}$_.coeffs=$G($_.coeffs,0,$_.k);for(var _2T=$_.k-1;_2T>=0;_2T-=2){var _2U=$_.coeffs;$p(_2U,_2T,929-$g(_2U,_2T))}$k[$j++]=$_.coeffs};$_.pdf417_coeffscachemax=9;$_.pdf417_coeffscachelimit=1022;$k[$j++]=$_.pdf417_coeffscachemax;$k[$j++]=$_.pdf417_coeffscachelimit;bwipp_fifocache();var _2Z=$k[--$j];$_.pdf417_coeffscache=_2Z;$_.pdf417_coeffscachefetch=function(){var _2a=$k[--$j];$_.k=_2a;var _2c=$g($_.pdf417_coeffscache,"fetch");$k[$j++]=$_.k;$k[$j++]=function(){$_.pdf417_gencoeffs()};$k[$j++]=function(){var _2d=$k[--$j];$k[$j++]=_2d.length};if(_2c()===true){return true}};$_.charvals=$a([new Map,new Map,new Map,new Map]);$_.alltext=new Map;for(var _2f=0,_2e=$_.pdf417_charmaps.length-1;_2f<=_2e;_2f+=1){$_.i=_2f;$_.encs=$g($_.pdf417_charmaps,$_.i);for(var _2i=0;_2i<=3;_2i+=1){$_.j=_2i;var _2l=$g($_.encs,$_.j);var _2m=$t(_2l);if($eq(_2m,"stringtype")){_2l=$g(_2l,0)}$p($g($_.charvals,$_.j),_2l,$_.i);$p($_.alltext,_2l,-1)}}$_.charvals=$_.charvals;$_.alltext=$_.alltext;$k[$j++]=Infinity;$k[$j++]=1;for(var _2x=0;_2x<928;_2x++){var _2y=$k[$j-1];$k[$j++]=_2y*3%929}var _2z=$a();$_.rsalog=_2z;$_.rslog=$a(929);for(var _31=1;_31<=928;_31+=1){$p($_.rslog,$g($_.rsalog,_31),_31)}$_.rslog=$_.rslog;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_pdf417.globals);$_.dontdraw=false;$_.compact=false;$_.eclevel=-1;$_.fixedeclevel=false;$_.columns=0;$_.rows=0;$_.rowmult=3;$_.ccc=false;$_.raw=false;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _36=$k[--$j];$_.options=_36;var _37=$k[--$j];$_.barcode=_37;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.pdf417emptyData#22667";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>12500){$k[$j++]="bwipp.pdf417inputTooLong#22671";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.eclevel!=-1&&($_.eclevel<0||$_.eclevel>8)){$k[$j++]="bwipp.pdf417InvalidErrorCorrectionLevel#22675";$k[$j++]="Valid error correction levels are 0 to 8";bwipp_raiseerror()}if($_.ccc&&$_.raw){$k[$j++]="bwipp.pdf417cccAndRaw#22679";$k[$j++]="Cannot combine ccc and raw";bwipp_raiseerror()}if($_.ccc&&$_.compact){$k[$j++]="bwipp.pdf417cccAndCompact#22683";$k[$j++]="Cannot combine ccc and compact";bwipp_raiseerror()}if($_.rows!=0&&($_.rows<3||$_.rows>90)){$k[$j++]="bwipp.pdf417invalidRows#22687";$k[$j++]="There must be between 3 and 90 rows";bwipp_raiseerror()}if($_.columns!=0&&($_.columns<1||$_.columns>30)){$k[$j++]="bwipp.pdf417invalidColumns#22691";$k[$j++]="There must be between 1 and 30 columns";bwipp_raiseerror()}if($_.rowmult<=0||$_.rowmult>50){$k[$j++]="bwipp.pdf417badRowMult#22695";$k[$j++]="The row multiplier must be greater than 0 and at most 50";bwipp_raiseerror()}if($_.raw){$_.datcws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _3b=$G($_.barcode,$_.i+1,3);$k[$j++]=_3b;$k[$j++]=false;for(var _3c=0,_3d=_3b.length;_3c<_3d;_3c++){var _3e=$g(_3b,_3c);if(_3e<48||_3e>57){$k[$j-1]=true}}var _3f=$k[--$j];if(_3f){$j--;break}var _3g=$k[--$j];$_.cw=$i(_3g);if($_.cw>928){$k[$j++]="bwipp.pdf417badRawCodeword#22714";$k[$j++]="Raw codewords must be 0 to 928";bwipp_raiseerror()}$p($_.datcws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.pdf417badRawFormat#22721";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.datcws=$G($_.datcws,0,$_.j)}$_.encb=function(){var _3s=$k[--$j];$_.in=_3s;$_.inlen=$_.in.length;$_.out=$a(~~($_.inlen/6)*5+$_.inlen%6);for(var _3z=0,_3y=~~($_.inlen/6)-1;_3z<=_3y;_3z+=1){$_.k=_3z;$k[$j++]=Infinity;$q($G($_.in,$_.k*6,3));$_.msbs=$a();$k[$j++]=Infinity;$q($_.msbs);var _44=$k[--$j];var _45=$k[--$j];var _46=$k[--$j];$k[$j++]=$f(_44+_45*256+_46*65536);for(var _47=0;_47<3;_47++){var _48=$k[--$j];$k[$j++]=_48%900;$k[$j++]=~~(_48/900)}$_.mscs=$a();$k[$j++]=Infinity;$q($G($_.in,$_.k*6+3,3));$_.lsbs=$a();$k[$j++]=Infinity;$q($_.lsbs);var _4D=$k[--$j];var _4E=$k[--$j];var _4F=$k[--$j];$k[$j++]=$f(_4D+_4E*256+_4F*65536);for(var _4G=0;_4G<3;_4G++){var _4H=$k[--$j];$k[$j++]=_4H%900;$k[$j++]=~~(_4H/900)}$_.lscs=$a();var _4J=$g($_.lscs,0);var _4L=$g($_.mscs,0);$p($_.out,$_.k*5+4,$f(_4J+_4L*316)%900);var _4P=$g($_.lscs,1);var _4R=$g($_.mscs,0);var _4T=$g($_.mscs,1);$p($_.out,$_.k*5+3,$f(~~((_4J+_4L*316)/900)+_4P+_4R*641+_4T*316)%900);var _4X=$g($_.lscs,2);var _4Z=$g($_.mscs,0);var _4b=$g($_.mscs,1);var _4d=$g($_.mscs,2);$p($_.out,$_.k*5+2,$f(~~((~~((_4J+_4L*316)/900)+_4P+_4R*641+_4T*316)/900)+_4X+_4Z*20+_4b*641+_4d*316)%900);var _4h=$g($_.lscs,3);var _4j=$g($_.mscs,1);var _4l=$g($_.mscs,2);$p($_.out,$_.k*5+1,$f(~~((~~((~~((_4J+_4L*316)/900)+_4P+_4R*641+_4T*316)/900)+_4X+_4Z*20+_4b*641+_4d*316)/900)+_4h+_4j*20+_4l*641)%900);$p($_.out,$_.k*5,$f(~~((~~((~~((~~((_4J+_4L*316)/900)+_4P+_4R*641+_4T*316)/900)+_4X+_4Z*20+_4b*641+_4d*316)/900)+_4h+_4j*20+_4l*641)/900)+$g($_.mscs,2)*20)%900)}$_.rem=$_.inlen%6;if($_.rem!=0){var _54=$_.out;var _53=$_.out.length-$_.rem;$k[$j++]=Infinity;$q($G($_.in,$_.inlen-$_.rem,$_.rem));var _52=$a();$P(_54,_53,_52)}$k[$j++]=$_.out};if($_.ccc){$_.barlen=$_.barcode.length;$_.datcws=$a(~~($_.barlen/6)*5+$_.barlen%6+2);$p($_.datcws,0,920);var _5E=$_.barlen%6==0?924:901;$p($_.datcws,1,_5E);$k[$j++]=$_.datcws;$k[$j++]=2;$k[$j++]=Infinity;$F($_.barcode);var _5H=$a();$k[$j++]=_5H;$_.encb();var _5I=$k[--$j];var _5J=$k[--$j];var _5K=$k[--$j];$P(_5K,_5J,_5I)}if(!$_.raw&&!$_.ccc){$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _5R=$k[--$j];$_.msg=_5R;$_.msglen=$_.msg.length;$k[$j++]=Infinity;for(var _5U=0,_5V=$_.msglen;_5U<_5V;_5U++){$k[$j++]=0}$k[$j++]=0;$_.numdigits=$a();$k[$j++]=Infinity;for(var _5X=0,_5Y=$_.msglen;_5X<_5Y;_5X++){$k[$j++]=0}$k[$j++]=0;$_.numtext=$a();$k[$j++]=Infinity;for(var _5a=0,_5b=$_.msglen;_5a<_5b;_5a++){$k[$j++]=0}$k[$j++]=0;$_.numbytes=$a();$k[$j++]=Infinity;for(var _5d=0,_5e=$_.msglen;_5d<_5e;_5d++){$k[$j++]=0}$k[$j++]=0;$_.iseci=$a();for(var _5g=$_.msglen-1;_5g>=0;_5g-=1){$_.i=_5g;var _5j=$g($_.msg,$_.i);if(_5j>=48&&_5j<=57){$p($_.numdigits,$_.i,$g($_.numdigits,$_.i+1)+1)}if($has($_.alltext,$g($_.msg,$_.i))&&$g($_.numdigits,$_.i)<13){$p($_.numtext,$_.i,$g($_.numtext,$_.i+1)+1)}if($g($_.msg,$_.i)>=0&&$g($_.numtext,$_.i)<5&&$g($_.numdigits,$_.i)<13){$p($_.numbytes,$_.i,$g($_.numbytes,$_.i+1)+1)}$p($_.iseci,$_.i,$g($_.msg,$_.i)<=-1e6)}$_.numdigits=$G($_.numdigits,0,$_.msglen);$_.numtext=$G($_.numtext,0,$_.msglen);$_.numbytes=$G($_.numbytes,0,$_.msglen);$k[$j++]=Infinity;$_.seqlen=0;$_.state=$_.pdf417_t;$_.p=0;for(;;){if($_.p==$_.msglen){break}if($g($_.iseci,$_.p)){$_.eci=$g($_.msg,$_.p);$_.p=$_.p+1;$k[$j++]=$a([$_.eci]);var _6k=$_.seqlen;var _6j=$_.eci<=-1810900?2:$_.eci<=-1000900?3:2;$_.seqlen=$f(_6k+_6j)}else{$_.n=$g($_.numdigits,$_.p);if($_.n>=13||$_.n==$_.msglen&&$_.n>=8){$k[$j++]=$_.pdf417_nl;$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.n));var _6x=$a();$_.state=$_.pdf417_n;$_.p=$f($_.p+$_.n);$_.seqlen=$f($_.seqlen+1+$_.n);$k[$j++]=_6x}else{$_.t=$g($_.numtext,$_.p);if($_.t>=5){if($_.state!=$_.pdf417_t){$k[$j++]=$_.pdf417_tl}$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.t));var _7B=$a();$_.state=$_.pdf417_t;$_.p=$f($_.p+$_.t);$_.seqlen=$f($_.seqlen+1+$_.t);$k[$j++]=_7B}else{$_.b=$g($_.numbytes,$_.p);if($_.b==1&&$_.state==$_.pdf417_t){$_.p=$f($_.p+$_.b);$_.seqlen=$_.seqlen+2;$k[$j++]=$_.pdf417_bs;$k[$j++]=$a([$g($_.msg,$_.p)])}else{if($_.b%6!=0){$k[$j++]=$_.pdf417_bl}else{$k[$j++]=$_.pdf417_bl6}$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.b));var _7X=$a();$_.state=$_.pdf417_b;$_.p=$f($_.p+$_.b);$_.seqlen=$f($_.seqlen+1+$_.b);$k[$j++]=_7X}}}}}$as($a($m()));var _7e=$k[--$j];$j--;$_.seq=_7e;$_.enca=function(){var _7j=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.pdf417_a),_7j)};$_.encl=function(){var _7n=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.pdf417_l),_7n)};$_.encm=function(){var _7r=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.pdf417_m),_7r)};$_.encp=function(){var _7v=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.pdf417_p),_7v)};$_.textencfuncs=$_.pdf417_textencfuncs;$_.addtotext=function(){var _7y=$k[--$j];$p($_.text,$_.l,_7y);$_.l=$_.l+1};$_.enct=function(){var _81=$k[--$j];$_.in=_81;$_.maxseqlen=$_.in.length*3+4;$k[$j++]=Infinity;for(var _83=0;_83<4;_83++){$k[$j++]=$a($_.maxseqlen)}$_.seq0=$a();$k[$j++]=Infinity;for(var _86=0;_86<4;_86++){$k[$j++]=$a($_.maxseqlen)}$_.seq1=$a();$_.len0=$a(4);$p($_.len0,0,0);$p($_.len0,1,0);$p($_.len0,2,0);$p($_.len0,3,0);$_.len1=$a(4);$_.curseq=$_.seq0;$_.curseqlen=$_.len0;$_.nxtseq=$_.seq1;$_.nxtseqlen=$_.len1;$_.curlen=$a([$_.pdf417_e,$_.pdf417_e,$_.pdf417_e,$_.pdf417_e]);$p($_.curlen,$_.submode,0);$F($_.in,function(){var _8M=$k[--$j];$_.char=_8M;for(;;){$_.imp=false;for(var _8N=0,_8O=$_.pdf417_textsubmodes.length;_8N<_8O;_8N++){$_.x=$g($_.pdf417_textsubmodes,_8N);for(var _8Q=0,_8R=$_.pdf417_textsubmodes.length;_8Q<_8R;_8Q++){$_.y=$g($_.pdf417_textsubmodes,_8Q);$_.cost=$f($g($_.curlen,$_.x)+$g($g($_.pdf417_latlen,$_.x),$_.y));if($_.cost<$g($_.curlen,$_.y)){$p($_.curlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.curseq,$_.y),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$F($g($g($_.pdf417_latseq,$_.x),$_.y),function(){$k[$j++]=$g($_.curseq,$_.y);$k[$j++]=$_.srclen;var _91=$k[$j-3];var _92=$k[$j-1];var _93=$k[$j-2];$j-=3;$p(_93,_92,_91);$_.srclen=$_.srclen+1});$p($_.curseqlen,$_.y,$_.srclen);$_.imp=true}}}if(!$_.imp){break}}$_.nxtlen=$a([$_.pdf417_e,$_.pdf417_e,$_.pdf417_e,$_.pdf417_e]);$p($_.nxtseqlen,0,0);$p($_.nxtseqlen,1,0);$p($_.nxtseqlen,2,0);$p($_.nxtseqlen,3,0);for(var _9D=0,_9E=$_.pdf417_textsubmodes.length;_9D<_9E;_9D++){$_.x=$g($_.pdf417_textsubmodes,_9D);for(;;){if(!$has($g($_.charvals,$_.x),$_.char)){break}$_.cost=$g($_.curlen,$_.x)+1;if($_.cost<$g($_.nxtlen,$_.x)){$p($_.nxtlen,$_.x,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.nxtseq,$_.x),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$p($g($_.nxtseq,$_.x),$_.srclen,$_.char);$p($_.nxtseqlen,$_.x,$_.srclen+1)}for(var _9p=0,_9q=$_.pdf417_textsubmodes.length;_9p<_9q;_9p++){$_.y=$g($_.pdf417_textsubmodes,_9p);if($ne($_.x,$_.y)){$_.cost=$f($g($_.curlen,$_.y)+$g($g($_.pdf417_shftlen,$_.y),$_.x))+1;if($_.cost<$g($_.nxtlen,$_.y)){$p($_.nxtlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.y);if($_.srclen>0){$P($g($_.nxtseq,$_.y),0,$G($g($_.curseq,$_.y),0,$_.srclen))}var _AR=$g($_.nxtseq,$_.y);var _AQ=$_.srclen;var _AP=$_.x==$_.pdf417_a?$_.pdf417_as:$_.pdf417_ps;$p(_AR,_AQ,_AP);$p($g($_.nxtseq,$_.y),$_.srclen+1,$_.char);$p($_.nxtseqlen,$_.y,$_.srclen+2)}}}break}}$_.curlen=$_.nxtlen;var _Ab=$_.curseq;$_.curseq=$_.nxtseq;$_.nxtseq=_Ab;var _Ad=$_.curseqlen;$_.curseqlen=$_.nxtseqlen;$_.nxtseqlen=_Ad});$_.minseq=$_.pdf417_e;for(var _Af=0,_Ag=$_.pdf417_textsubmodes.length;_Af<_Ag;_Af++){$_.k=$g($_.pdf417_textsubmodes,_Af);if($g($_.curlen,$_.k)<$_.minseq){$_.minseq=$g($_.curlen,$_.k);$_.txtseq=$G($g($_.curseq,$_.k),0,$g($_.curseqlen,$_.k))}}$_.text=$a($_.minseq);$_.k=0;$_.l=0;for(;;){if($_.k>=$_.txtseq.length){break}$_.char=$g($_.txtseq,$_.k);var _B7=$_[$g($_.textencfuncs,$_.submode)];$k[$j++]=$_.char;if(_B7()===true){break}$_.addtotext();$_.k=$_.k+1;if($_.char==$_.pdf417_as||$_.char==$_.pdf417_ps){$k[$j++]=$g($_.txtseq,$_.k);if($_.char==$_.pdf417_as){$_.enca()}else{$_.encp()}$_.addtotext();$_.k=$_.k+1}if($_.char==$_.pdf417_al){$_.submode=$_.pdf417_a}if($_.char==$_.pdf417_ll){$_.submode=$_.pdf417_l}if($_.char==$_.pdf417_ml){$_.submode=$_.pdf417_m}if($_.char==$_.pdf417_pl){$_.submode=$_.pdf417_p}}if($_.text.length%2==1){if($_.submode==$_.pdf417_p){$k[$j++]="pad";$k[$j++]=$_.pdf417_al;$_.encp();var _BM=$k[--$j];var _BN=$k[--$j];$_[_BN]=_BM;$_.submode=$_.pdf417_a}else{$_.nextecitext=false;for(var _BR=$_.i+1,_BQ=$_.seq.length-1;_BR<=_BQ;_BR+=1){$_.ti=_BR;if($ne($t($g($_.seq,$_.ti)),"arraytype")){$_.nextecitext=false;break}if($g($g($_.seq,$_.ti),0)>-1e6){break}$_.nextecitext=true}if($_.nextecitext){if($_.submode==$_.pdf417_a){if($has($g($_.charvals,$_.pdf417_l),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.pdf417_ll;$_.enca();var _Bj=$k[--$j];var _Bk=$k[--$j];$_[_Bk]=_Bj;$_.submode=$_.pdf417_l}else{$k[$j++]="pad";$k[$j++]=$_.pdf417_ml;$_.enca();var _Bl=$k[--$j];var _Bm=$k[--$j];$_[_Bm]=_Bl;$_.submode=$_.pdf417_m}}else{if($_.submode==$_.pdf417_m){if($has($g($_.charvals,$_.pdf417_l),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.pdf417_ll;$_.encm();var _Bv=$k[--$j];var _Bw=$k[--$j];$_[_Bw]=_Bv;$_.submode=$_.pdf417_l}else{if($has($g($_.charvals,$_.pdf417_p),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.pdf417_pl;$_.encm();var _C4=$k[--$j];var _C5=$k[--$j];$_[_C5]=_C4;$_.submode=$_.pdf417_p}else{$k[$j++]="pad";$k[$j++]=$_.pdf417_al;$_.encm();var _C6=$k[--$j];var _C7=$k[--$j];$_[_C7]=_C6;$_.submode=$_.pdf417_a}}}else{$k[$j++]="pad";$k[$j++]=$_.pdf417_ml;$_.encl();var _C8=$k[--$j];var _C9=$k[--$j];$_[_C9]=_C8;$_.submode=$_.pdf417_m}}}else{var _CD=$_[$g($_.textencfuncs,$_.submode)];$k[$j++]="pad";$k[$j++]=$_.pdf417_ps;if(_CD()===true){return true}var _CE=$k[--$j];var _CF=$k[--$j];$_[_CF]=_CE}}$k[$j++]=Infinity;$q($_.text);$k[$j++]=$_.pad;$_.text=$a()}$_.out=$a(~~($_.text.length/2));for(var _CM=0,_CL=$_.out.length-1;_CM<=_CL;_CM+=1){$_.k=_CM;$p($_.out,$_.k,$f($g($_.text,$_.k*2)*30+$g($_.text,$_.k*2+1)))}$k[$j++]=$_.out};$_.encn=function(){var _CW=$k[--$j];$_.in=_CW;$k[$j++]=Infinity;for(var _CZ=0,_CY=$_.in.length-1;_CZ<=_CY;_CZ+=44){$_.k=_CZ;$k[$j++]=Infinity;var _Ce=$_.in.length-$_.k;$k[$j++]=1;var _Ch=$_.in;var _Cg=$_.k;if(_Ce>44){_Ce=44}var _Ci=$G(_Ch,_Cg,_Ce);for(var _Cj=0,_Ck=_Ci.length;_Cj<_Ck;_Cj++){$k[$j++]=$g(_Ci,_Cj)-48}$_.gmod=$a();$_.cwn=$a(15);$_.cwi=15;for(;;){$_.dv=900;$_.val=0;$_.gmulstart=true;$k[$j++]=Infinity;for(;;){if($_.gmod.length==0){break}$_.val=$f($_.val*10+$g($_.gmod,0));$_.gmod=$G($_.gmod,1,$_.gmod.length-1);if($_.val<$_.dv){if(!$_.gmulstart){$k[$j++]=0}}else{$_.gmulstart=false;$k[$j++]=~~($_.val/$_.dv)}$_.val=$_.val%$_.dv}$as($a($m()));var _D3=$k[--$j];$j--;$_.cwi=$_.cwi-1;$p($_.cwn,$_.cwi,$_.val);$_.gmod=_D3;if($_.gmod.length==0){break}}$q($G($_.cwn,$_.cwi,15-$_.cwi))}$as($a($m()));var _DH=$k[--$j];$k[$j-1]=_DH};$_.ence=function(){var _DJ=$k[--$j];var _DL=-$g(_DJ,0)-1e6;$k[$j++]=_DL;if(_DL<=899){var _DM=$k[--$j];var _DN=$a(2);$k[$j++]=927;$k[$j++]=_DM;$as(_DN)}else{var _DO=$k[$j-1];if(_DO<=810899){var _DP=$k[--$j];var _DQ=$a(3);$k[$j++]=926;$k[$j++]=~~(_DP/900)-1;$k[$j++]=_DP%900;$as(_DQ)}else{var _DR=$k[$j-1];if(_DR<=811799){var _DS=$k[--$j];var _DT=$a(2);$k[$j++]=925;$k[$j++]=_DS-810900;$as(_DT)}else{$k[$j-1]="bwipp.pdf417badECI#23137";$k[$j++]="PDF417 supports ECIs 000000 to 811799";bwipp_raiseerror()}}}};$_.encfuncs=$_.pdf417_encfuncs;$_.addtocws=function(){var _DU=$k[--$j];$P($_.datcws,$_.j,_DU);$_.j=_DU.length+$_.j};$_.state=$_.pdf417_t;$_.submode=$_.pdf417_a;$_.datcws=$a($_.seqlen);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.seq.length){break}$_.chars=$g($_.seq,$_.i);if($eq($t($_.chars),"arraytype")){if($g($_.chars,0)<=-1e6){$k[$j++]=$_.chars;$_.ence();$_.addtocws()}else{var _Do=$_[$g($_.encfuncs,$_.state)];$k[$j++]=$_.chars;if(_Do()===true){break}$_.addtocws()}}else{$k[$j++]=$a([$g($_.pdf417_latchcws,$_.chars)]);$_.addtocws();if($_.chars==$_.pdf417_tl){$_.state=$_.pdf417_t;$_.submode=$_.pdf417_a}if($_.chars==$_.pdf417_nl){$_.state=$_.pdf417_n}if($_.chars==$_.pdf417_bl||$_.chars==$_.pdf417_bl6){$_.state=$_.pdf417_b}if($_.chars==$_.pdf417_bs){$_.i=$_.i+1;$k[$j++]=$g($_.seq,$_.i);$_.encb();$_.addtocws()}}$_.i=$_.i+1}$_.datcws=$G($_.datcws,0,$_.j)}$_.m=$_.datcws.length;if($_.m>926){$k[$j++]="bwipp.pdf417dataTooLong#23182";$k[$j++]="The data is too long";bwipp_raiseerror()}if($_.eclevel==-1){if($_.m<=40){$_.eclevel=2}if($_.m>=41&&$_.m<=160){$_.eclevel=3}if($_.m>=161&&$_.m<=320){$_.eclevel=4}if($_.m>=321){$_.eclevel=5}}if(!$_.fixedeclevel){$_.maxeclevel=~~($log(928-1-$_.m)/$log(2))-1;if($_.eclevel>$_.maxeclevel){$_.eclevel=$_.maxeclevel}}$_.k=$pow(2,$_.eclevel+1);if($_.columns==0){$_.columns=~~$rnd($sqrt($f($_.m+$_.k)/3))}var _EP=$_.columns>=1?$_.columns:1;$_.c=_EP;$_.r=$ceil(($f($_.m+$_.k)+1)/$_.columns);if($_.r<$_.rows&&$_.rows<=90){$_.r=$_.rows}if($_.r<3){$_.r=3}if($_.r>90){$k[$j++]="bwipp.pdf417insufficientCapacity#23221";$k[$j++]="Insufficient capacity in the symbol";bwipp_raiseerror()}if(!$_.fixedeclevel){var _Eb=$_.c;var _Ec=$_.r;var _Ed=$_.m;var _Ee=8;var _Ef=~~($log($f(_Eb*_Ec-1-_Ed))/$log(2))-1;if(~~($log($f(_Eb*_Ec-1-_Ed))/$log(2))-1>8){var _=_Ee;_Ee=_Ef;_Ef=_}$_.maxeclevel=_Ef;if($_.maxeclevel>$_.eclevel){$_.eclevel=$_.maxeclevel;$_.k=$pow(2,$_.eclevel+1)}}$k[$j++]="coeffs";$k[$j++]=$_.k;$_.pdf417_coeffscachefetch();var _El=$k[--$j];var _Em=$k[--$j];$_[_Em]=_El;$_.n=$f($_.c*$_.r-$_.k);if($_.n>928){$k[$j++]="bwipp.pdf417insufficientCapacity#23249";$k[$j++]="Insufficient capacity in the symbol";bwipp_raiseerror()}$_.cws=$a($_.c*$_.r);$p($_.cws,0,$_.n);$P($_.cws,1,$_.datcws);var _F6=$_.cws;var _F5=$_.m+1;var _F4=[];for(var _F2=0,_F3=$f($_.n-$_.m)-1;_F2<_F3;_F2++){_F4[_F2]=900}_F4=$a(_F4);$P(_F6,_F5,_F4);if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#23258";$k[$j++]=$G($_.cws,0,$_.n);bwipp_raiseerror()}var _FC=$_.n;var _FD=$_.coeffs;var _FE=$_.k;var _FI=$G($_.cws,$_.n,$_.k);$k[$j++]=function(){var _FK=$k[--$j];$k[$j++]=$g($_.cws,_FK)};$k[$j++]=_FC;$k[$j++]=_FD;$k[$j++]=_FE;$k[$j++]=_FI;$k[$j++]=929;bwipp_rsecprime();if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#23267";$k[$j++]=$G($_.cws,$_.n,$_.k);bwipp_raiseerror()}$_.cwtobits=function(){var _FS=$k[--$j];var _FU=$k[--$j];$_.v=$g($g($_.pdf417_clusters,_FS),_FU);$k[$j++]=Infinity;for(var _FW=0;_FW<17;_FW++){$k[$j++]=0}var _FZ=$R($s(17),$_.v,2);for(var _Fa=0,_Fb=_FZ.length;_Fa<_Fb;_Fa++){$k[$j++]=$g(_FZ,_Fa)-48}var _Fd=$a();$k[$j++]=$G(_Fd,_Fd.length-17,17)};if($_.compact){$_.rwid=17*$_.c+17+17+1}else{$_.rwid=17*$_.c+17+17+17+18}$k[$j++]=$_.rwid*$_.r;try{var _Fk=$k[--$j];$k[$j++]=$a(_Fk);var _Fm=false}catch(e){$k[$j++]=true}if(_Fm){$k[$j-1]="bwipp.pdf417PixsTooLarge#23291";$k[$j++]="The resulting symbol exceeds the implementation maximum array size";bwipp_raiseerror()}var _Fn=$k[--$j];$_.pixs=_Fn;for(var _Fq=0,_Fp=$_.r-1;_Fq<=_Fp;_Fq+=1){$_.i=_Fq;if($_.i%3==0){$_.lcw=~~($_.i/3)*30+~~(($_.r-1)/3);$_.rcw=$f(~~($_.i/3)*30+$_.c)-1}if($_.i%3==1){$_.lcw=~~($_.i/3)*30+$_.eclevel*3+($_.r-1)%3;$_.rcw=~~($_.i/3)*30+~~(($_.r-1)/3)}if($_.i%3==2){$_.lcw=$f(~~($_.i/3)*30+$_.c)-1;$_.rcw=~~($_.i/3)*30+$_.eclevel*3+($_.r-1)%3}var _GU=$_.pixs;var _GT=$_.i*$_.rwid;$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=$_.lcw;$k[$j++]=$_.i%3;$_.cwtobits();var _GD=$k[--$j];$q(_GD);var _GI=$G($_.cws,$_.c*$_.i,$_.c);for(var _GJ=0,_GK=_GI.length;_GJ<_GK;_GJ++){$k[$j++]=$g(_GI,_GJ);$k[$j++]=$_.i%3;$_.cwtobits();var _GN=$k[--$j];$q(_GN)}if($_.compact){$k[$j++]=1}else{$k[$j++]=$_.rcw;$k[$j++]=$_.i%3;$_.cwtobits();var _GR=$k[--$j];$q(_GR);$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=1}var _GS=$a();$P(_GU,_GT,_GS)}$k[$j++]=Infinity;var _GV=$_.pixs;var _GW=$_.rwid;var _GX=$_.r;var _GY=$_.rowmult;var _Gd=[];for(var _Ga=0,_Gb=$_.r;_Ga<_Gb;_Ga++){_Gd[_Ga]=$_.rowmult}_Gd=$a(_Gd);$k[$j++]="ren";$k[$j++]="renmatrix";$k[$j++]="pixs";$k[$j++]=_GV;$k[$j++]="pixx";$k[$j++]=_GW;$k[$j++]="pixy";$k[$j++]=_GX*_GY;$k[$j++]="rowmult";$k[$j++]=_Gd;$k[$j++]="height";$k[$j++]=$_.r*$_.rowmult/72;$k[$j++]="width";$k[$j++]=$_.rwid/72;$k[$j++]="borderleft";$k[$j++]=2;$k[$j++]="borderright";$k[$j++]=2;$k[$j++]="bordertop";$k[$j++]=2;$k[$j++]="borderbottom";$k[$j++]=2;$k[$j++]="opt";$k[$j++]=$_.options;var _Gi=$d();$k[$j++]=_Gi;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_pdf417compact(){var $__=$_;$_=Object.assign({},$_,bwipp_pdf417compact.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"compact",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_pdf417();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_micropdf417(){if(!bwipp_micropdf417.globals){var $__=$_;$_=bwipp_micropdf417.globals={};$_.micropdf417_t=0;$_.micropdf417_n=1;$_.micropdf417_b=2;$_.micropdf417_a=0;$_.micropdf417_l=1;$_.micropdf417_m=2;$_.micropdf417_p=3;$_.micropdf417_tl=-1;$_.micropdf417_nl=-2;$_.micropdf417_bl=-3;$_.micropdf417_bl6=-4;$_.micropdf417_bs=-5;$_.micropdf417_al=-6;$_.micropdf417_ll=-7;$_.micropdf417_ml=-8;$_.micropdf417_pl=-9;$_.micropdf417_as=-10;$_.micropdf417_ps=-11;$_.al=$_.micropdf417_al;$_.ll=$_.micropdf417_ll;$_.ml=$_.micropdf417_ml;$_.pl=$_.micropdf417_pl;$_.as=$_.micropdf417_as;$_.ps=$_.micropdf417_ps;$_.micropdf417_m05=-12;$_.micropdf417_m06=-13;$_.micropdf417_textsubmodes=$a([$_.micropdf417_a,$_.micropdf417_l,$_.micropdf417_m,$_.micropdf417_p]);$_.micropdf417_charmaps=$a([$a(["A","a","0",";"]),$a(["B","b","1","<"]),$a(["C","c","2",">"]),$a(["D","d","3","@"]),$a(["E","e","4","["]),$a(["F","f","5",92]),$a(["G","g","6","]"]),$a(["H","h","7","_"]),$a(["I","i","8","`"]),$a(["J","j","9","~"]),$a(["K","k","&","!"]),$a(["L","l",13,13]),$a(["M","m",9,9]),$a(["N","n",",",","]),$a(["O","o",":",":"]),$a(["P","p","#",10]),$a(["Q","q","-","-"]),$a(["R","r",".","."]),$a(["S","s","$","$"]),$a(["T","t","/","/"]),$a(["U","u","+",'"']),$a(["V","v","%","|"]),$a(["W","w","*","*"]),$a(["X","x","=",40]),$a(["Y","y","^",41]),$a(["Z","z",$_.pl,"?"]),$a([" "," "," ","{"]),$a([$_.ll,$_.as,$_.ll,"}"]),$a([$_.ml,$_.ml,$_.al,"'"]),$a([$_.ps,$_.ps,$_.ps,$_.al])]);$_.micropdf417_e=2e4;$_.e=$_.micropdf417_e;$_.micropdf417_latlen=$a([$a([0,1,1,2]),$a([2,0,1,2]),$a([1,1,0,1]),$a([1,2,2,0])]);$_.micropdf417_latseq=$a([$a([$a([]),$a([$_.ll]),$a([$_.ml]),$a([$_.ml,$_.pl])]),$a([$a([$_.ml,$_.al]),$a([]),$a([$_.ml]),$a([$_.ml,$_.pl])]),$a([$a([$_.al]),$a([$_.ll]),$a([]),$a([$_.pl])]),$a([$a([$_.al]),$a([$_.al,$_.ll]),$a([$_.al,$_.ml]),$a([])])]);$_.micropdf417_shftlen=$a([$a([$_.e,$_.e,$_.e,1]),$a([1,$_.e,$_.e,1]),$a([$_.e,$_.e,$_.e,1]),$a([$_.e,$_.e,$_.e,$_.e])]);$_.micropdf417_ccametrics=$a([$a([2,5,4,39,0,19]),$a([2,6,4,1,0,33]),$a([2,7,5,32,0,12]),$a([2,8,5,8,0,40]),$a([2,9,6,14,0,46]),$a([2,10,6,43,0,23]),$a([2,12,7,20,0,52]),$a([3,4,4,11,43,23]),$a([3,5,5,1,33,13]),$a([3,6,6,5,37,17]),$a([3,7,7,15,47,27]),$a([3,8,7,21,1,33]),$a([4,3,4,40,20,52]),$a([4,4,5,43,23,3]),$a([4,5,6,46,26,6]),$a([4,6,7,34,14,46]),$a([4,7,8,29,9,41])]);$_.micropdf417_nonccametrics=$a([$a([1,11,7,1,0,9]),$a([1,14,7,8,0,8]),$a([1,17,7,36,0,36]),$a([1,20,8,19,0,19]),$a([1,24,8,9,0,17]),$a([1,28,8,25,0,33]),$a([2,8,8,1,0,1]),$a([2,11,9,1,0,9]),$a([2,14,9,8,0,8]),$a([2,17,10,36,0,36]),$a([2,20,11,19,0,19]),$a([2,23,13,9,0,17]),$a([2,26,15,27,0,35]),$a([3,6,12,1,1,1]),$a([3,8,14,7,7,7]),$a([3,10,16,15,15,15]),$a([3,12,18,25,25,25]),$a([3,15,21,37,37,37]),$a([3,20,26,1,17,33]),$a([3,26,32,1,9,17]),$a([3,32,38,21,29,37]),$a([3,38,44,15,31,47]),$a([3,44,50,1,25,49]),$a([4,4,8,47,19,43]),$a([4,6,12,1,1,1]),$a([4,8,14,7,7,7]),$a([4,10,16,15,15,15]),$a([4,12,18,25,25,25]),$a([4,15,21,37,37,37]),$a([4,20,26,1,17,33]),$a([4,26,32,1,9,17]),$a([4,32,38,21,29,37]),$a([4,38,44,15,31,47]),$a([4,44,50,1,25,49])]);$_.micropdf417_clusters=$a([$a([120256,125680,128380,120032,125560,128318,108736,119920,108640,86080,108592,86048,110016,120560,125820,109792,120440,125758,88256,109680,88160,89536,110320,120700,89312,110200,120638,89200,110140,89840,110460,89720,110398,89980,128506,119520,125304,128190,107712,119408,125244,107616,119352,84032,107568,119324,84e3,107544,83984,108256,119672,125374,85184,108144,119612,85088,108088,119582,85040,108060,85728,108408,119742,85616,108348,85560,108318,85880,108478,85820,85790,107200,119152,125116,107104,119096,125086,83008,107056,119068,82976,107032,82960,82952,83648,107376,119228,83552,107320,119198,83504,107292,83480,83468,83824,107452,83768,107422,83740,83900,106848,118968,125022,82496,106800,118940,82464,106776,118926,82448,106764,82440,106758,82784,106936,119006,82736,106908,82712,106894,82700,82694,106974,82830,82240,106672,118876,82208,106648,118862,82192,106636,82184,106630,82180,82352,82328,82316,82080,118830,106572,106566,82050,117472,124280,127678,103616,117360,124220,103520,117304,124190,75840,103472,75808,104160,117624,124350,76992,104048,117564,76896,103992,76848,76824,77536,104312,117694,77424,104252,77368,77340,77688,104382,77628,77758,121536,126320,128700,121440,126264,128670,111680,121392,126236,111648,121368,126222,111632,121356,103104,117104,124092,112320,103008,117048,124062,112224,121656,126366,93248,74784,102936,117006,93216,112152,93200,75456,103280,117180,93888,75360,103224,117150,93792,112440,121758,93744,75288,93720,75632,103356,94064,75576,103326,94008,112542,93980,75708,94140,75678,94110,121184,126136,128606,111168,121136,126108,111136,121112,126094,111120,121100,111112,111108,102752,116920,123998,111456,102704,116892,91712,74272,121244,116878,91680,74256,102668,91664,111372,102662,74244,74592,102840,116958,92e3,74544,102812,91952,111516,102798,91928,74508,74502,74680,102878,92088,74652,92060,74638,92046,92126,110912,121008,126044,110880,120984,126030,110864,120972,110856,120966,110852,110850,74048,102576,116828,90944,74016,102552,116814,90912,111e3,121038,90896,73992,102534,90888,110982,90884,74160,102620,91056,74136,102606,91032,111054,91020,74118,91014,91100,91086,110752,120920,125998,110736,120908,110728,120902,110724,110722,73888,102488,116782,90528,73872,102476,90512,110796,102470,90504,73860,90500,73858,73944,90584,90572,90566,120876,120870,110658,102444,73800,90312,90308,90306,101056,116080,123580,100960,116024,70720,100912,115996,70688,100888,70672,70664,71360,101232,116156,71264,101176,116126,71216,101148,71192,71180,71536,101308,71480,101278,71452,71612,71582,118112,124600,127838,105024,118064,124572,104992,118040,124558,104976,118028,104968,118022,100704,115896,123486,105312,100656,115868,79424,70176,118172,115854,79392,105240,100620,79376,70152,79368,70496,100792,115934,79712,70448,118238,79664,105372,100750,79640,70412,79628,70584,100830,79800,70556,79772,70542,70622,79838,122176,126640,128860,122144,126616,128846,122128,126604,122120,126598,122116,104768,117936,124508,113472,104736,126684,124494,113440,122264,126670,113424,104712,117894,113416,122246,104706,69952,100528,115804,78656,69920,100504,115790,96064,78624,104856,117966,96032,113560,122318,100486,96016,78600,104838,96008,69890,70064,100572,78768,70040,100558,96176,78744,104910,96152,113614,70022,78726,70108,78812,70094,96220,78798,122016,126552,128814,122e3,126540,121992,126534,121988,121986,104608,117848,124462,113056,104592,126574,113040,122060,117830,113032,104580,113028,104578,113026,69792,100440,115758,78240,69776,100428,95136,78224,104652,100422,95120,113100,69764,95112,78212,69762,78210,69848,100462,78296,69836,95192,78284,69830,95180,78278,69870,95214,121936,126508,121928,126502,121924,121922,104528,117804,112848,104520,117798,112840,121958,112836,104514,112834,69712,100396,78032,69704,100390,94672,78024,104550,94664,112870,69698,94660,78018,94658,78060,94700,94694,126486,121890,117782,104484,104482,69672,77928,94440,69666,77922,99680,68160,99632,68128,99608,115342,68112,99596,68104,99590,68448,99768,115422,68400,99740,68376,99726,68364,68358,68536,99806,68508,68494,68574,101696,116400,123740,101664,116376,101648,116364,101640,116358,101636,67904,99504,115292,72512,67872,116444,115278,72480,101784,116430,72464,67848,99462,72456,101766,67842,68016,99548,72624,67992,99534,72600,101838,72588,67974,68060,72668,68046,72654,118432,124760,127918,118416,124748,118408,124742,118404,118402,101536,116312,105888,101520,116300,105872,118476,116294,105864,101508,105860,101506,105858,67744,99416,72096,67728,116334,80800,72080,101580,99398,80784,105932,67716,80776,72068,67714,72066,67800,99438,72152,67788,80856,72140,67782,80844,72134,67822,72174,80878,126800,128940,126792,128934,126788,126786,118352,124716,122576,126828,124710,122568,126822,122564,118338,122562,101456,116268,105680,101448,116262,114128,105672,118374,114120,122598,101442,114116,105666,114114,67664,99372,71888,67656,99366,80336,71880,101478,97232,80328,105702,67650,97224,114150,71874,97220,67692,71916,67686,80364,71910,97260,80358,97254,126760,128918,126756,126754,118312,124694,122472,126774,122468,118306,122466,101416,116246,105576,101412,113896,105572,101410,113892,105570,113890,67624,99350,71784,101430,80104,71780,67618,96744,80100,71778,96740,80098,96738,71798,96758,126738,122420,122418,105524,113780,113778,71732,79988,96500,96498,66880,66848,98968,66832,66824,66820,66992,66968,66956,66950,67036,67022,1e5,99984,115532,99976,115526,99972,99970,66720,98904,69024,100056,98892,69008,100044,69e3,100038,68996,66690,68994,66776,98926,69080,100078,69068,66758,69062,66798,69102,116560,116552,116548,116546,99920,102096,116588,115494,102088,116582,102084,99906,102082,66640,68816,66632,98854,73168,68808,66628,73160,68804,66626,73156,68802,66668,68844,66662,73196,68838,73190,124840,124836,124834,116520,118632,124854,118628,116514,118626,99880,115478,101992,116534,106216,101988,99874,106212,101986,106210,66600,98838,68712,99894,72936,68708,66594,81384,72932,68706,81380,72930,66614,68726,72950,81398,128980,128978,124820,126900,124818,126898,116500,118580,116498,122740,118578,122738,99860,101940,99858,106100,101938,114420]),$a([128352,129720,125504,128304,129692,125472,128280,129678,125456,128268,125448,128262,125444,125792,128440,129758,120384,125744,128412,120352,125720,128398,120336,125708,120328,125702,120324,120672,125880,128478,110144,120624,125852,110112,120600,125838,110096,120588,110088,120582,110084,110432,120760,125918,89664,110384,120732,89632,110360,120718,89616,110348,89608,110342,89952,110520,120798,89904,110492,89880,110478,89868,90040,110558,90012,89998,125248,128176,129628,125216,128152,129614,125200,128140,125192,128134,125188,125186,119616,125360,128220,119584,125336,128206,119568,125324,119560,125318,119556,119554,108352,119728,125404,108320,119704,125390,108304,119692,108296,119686,108292,108290,85824,108464,119772,85792,108440,119758,85776,108428,85768,108422,85764,85936,108508,85912,108494,85900,85894,85980,85966,125088,128088,129582,125072,128076,125064,128070,125060,125058,119200,125144,128110,119184,125132,119176,125126,119172,119170,107424,119256,125166,107408,119244,107400,119238,107396,107394,83872,107480,119278,83856,107468,83848,107462,83844,83842,83928,107502,83916,83910,83950,125008,128044,125e3,128038,124996,124994,118992,125036,118984,125030,118980,118978,106960,119020,106952,119014,106948,106946,82896,106988,82888,106982,82884,82882,82924,82918,124968,128022,124964,124962,118888,124982,118884,118882,106728,118902,106724,106722,82408,106742,82404,82402,124948,124946,118836,118834,106612,106610,124224,127664,129372,124192,127640,129358,124176,127628,124168,127622,124164,124162,117568,124336,127708,117536,124312,127694,117520,124300,117512,124294,117508,117506,104256,117680,124380,104224,117656,124366,104208,117644,104200,117638,104196,104194,77632,104368,117724,77600,104344,117710,77584,104332,77576,104326,77572,77744,104412,77720,104398,77708,77702,77788,77774,128672,129880,93168,128656,129868,92664,128648,129862,92412,128644,128642,124064,127576,129326,126368,124048,129902,126352,128716,127558,126344,124036,126340,124034,126338,117152,124120,127598,121760,117136,124108,121744,126412,124102,121736,117124,121732,117122,121730,103328,117208,124142,112544,103312,117196,112528,121804,117190,112520,103300,112516,103298,112514,75680,103384,117230,94112,75664,103372,94096,112588,103366,94088,75652,94084,75650,75736,103406,94168,75724,94156,75718,94150,75758,128592,129836,91640,128584,129830,91388,128580,91262,128578,123984,127532,126160,123976,127526,126152,128614,126148,123970,126146,116944,124012,121296,116936,124006,121288,126182,121284,116930,121282,102864,116972,111568,102856,116966,111560,121318,111556,102850,111554,74704,102892,92112,74696,102886,92104,111590,92100,74690,92098,74732,92140,74726,92134,128552,129814,90876,128548,90750,128546,123944,127510,126056,128566,126052,123938,126050,116840,123958,121064,116836,121060,116834,121058,102632,116854,111080,121078,111076,102626,111074,74216,102646,91112,74212,91108,74210,91106,74230,91126,128532,90494,128530,123924,126004,123922,126002,116788,120948,116786,120946,102516,110836,102514,110834,73972,90612,73970,90610,128522,123914,125978,116762,120890,102458,110714,123552,127320,129198,123536,127308,123528,127302,123524,123522,116128,123608,127342,116112,123596,116104,123590,116100,116098,101280,116184,123630,101264,116172,101256,116166,101252,101250,71584,101336,116206,71568,101324,71560,101318,71556,71554,71640,101358,71628,71622,71662,127824,129452,79352,127816,129446,79100,127812,78974,127810,123472,127276,124624,123464,127270,124616,127846,124612,123458,124610,115920,123500,118224,115912,123494,118216,124646,118212,115906,118210,100816,115948,105424,100808,115942,105416,118246,105412,100802,105410,70608,100844,79824,70600,100838,79816,105446,79812,70594,79810,70636,79852,70630,79846,129960,95728,113404,129956,95480,113278,129954,95356,95294,127784,129430,78588,128872,129974,95996,78462,128868,127778,95870,128866,123432,127254,124520,123428,126696,128886,123426,126692,124514,126690,115816,123446,117992,115812,122344,117988,115810,122340,117986,122338,100584,115830,104936,100580,113640,104932,100578,113636,104930,113634,70120,100598,78824,70116,96232,78820,70114,96228,78818,96226,70134,78838,129940,94968,113022,129938,94844,94782,127764,78206,128820,127762,95102,128818,123412,124468,123410,126580,124466,126578,115764,117876,115762,122100,117874,122098,100468,104692,100466,113140,104690,113138,69876,78324,69874,95220,78322,95218,129930,94588,94526,127754,128794,123402,124442,126522,115738,117818,121978,100410,104570,112890,69754,78074,94714,94398,123216,127148,123208,127142,123204,123202,115408,123244,115400,123238,115396,115394,99792,115436,99784,115430,99780,99778,68560,99820,68552,99814,68548,68546,68588,68582,127400,129238,72444,127396,72318,127394,123176,127126,123752,123172,123748,123170,123746,115304,123190,116456,115300,116452,115298,116450,99560,115318,101864,99556,101860,99554,101858,68072,99574,72680,68068,72676,68066,72674,68086,72694,129492,80632,105854,129490,80508,80446,127380,72062,127924,127378,80766,127922,123156,123700,123154,124788,123698,124786,115252,116340,115250,118516,116338,118514,99444,101620,99442,105972,101618,105970,67828,72180,67826,80884,72178,80882,97008,114044,96888,113982,96828,96798,129482,80252,130010,97148,80190,97086,127370,127898,128954,123146,123674,124730,126842,115226,116282,118394,122618,99386,101498,105722,114170,67706,71930,80378,96632,113854,96572,96542,80062,96702,96444,96414,96350,123048,123044,123042,115048,123062,115044,115042,99048,115062,99044,99042,67048,99062,67044,67042,67062,127188,68990,127186,123028,123316,123026,123314,114996,115572,114994,115570,98932,100084,98930,100082,66804,69108,66802,69106,129258,73084,73022,127178,127450,123018,123290,123834,114970,115514,116602,98874,99962,102138,66682,68858,73210,81272,106174,81212,81182,72894,81342,97648,114364,97592,114334,97564,97550,81084,97724,81054,97694,97464,114270,97436,97422,80990,97502,97372,97358,97326,114868,114866,98676,98674,66292,66290,123098,114842,115130,98618,99194,66170,67322,69310,73404,73374,81592,106334,81564,81550,73310,81630,97968,114524,97944,114510,97932,97926,81500,98012,81486,97998,97880,114478,97868,97862,81454,97902,97836,97830,69470,73564,73550,81752,106414,81740,81734,73518,81774,81708,81702]),$a([109536,120312,86976,109040,120060,86496,108792,119934,86256,108668,86136,129744,89056,110072,129736,88560,109820,129732,88312,109694,129730,88188,128464,129772,89592,128456,129766,89340,128452,89214,128450,125904,128492,125896,128486,125892,125890,120784,125932,120776,125926,120772,120770,110544,120812,110536,120806,110532,84928,108016,119548,84448,107768,119422,84208,107644,84088,107582,84028,129640,85488,108284,129636,85240,108158,129634,85116,85054,128232,129654,85756,128228,85630,128226,125416,128246,125412,125410,119784,125430,119780,119778,108520,119798,108516,108514,83424,107256,119166,83184,107132,83064,107070,83004,82974,129588,83704,107390,129586,83580,83518,128116,83838,128114,125172,125170,119284,119282,107508,107506,82672,106876,82552,106814,82492,82462,129562,82812,82750,128058,125050,119034,82296,106686,82236,82206,82366,82108,82078,76736,103920,117500,76256,103672,117374,76016,103548,75896,103486,75836,129384,77296,104188,129380,77048,104062,129378,76924,76862,127720,129398,77564,127716,77438,127714,124392,127734,124388,124386,117736,124406,117732,117730,104424,117750,104420,104418,112096,121592,126334,92608,111856,121468,92384,111736,121406,92272,111676,92216,111646,92188,75232,103160,117118,93664,74992,103036,93424,112252,102974,93304,74812,93244,74782,93214,129332,75512,103294,129908,129330,93944,75388,129906,93820,75326,93758,127604,75646,128756,127602,94078,128754,124148,126452,124146,126450,117236,121844,117234,121842,103412,103410,91584,111344,121212,91360,111224,121150,91248,111164,91192,111134,91164,91150,74480,102780,91888,74360,102718,91768,111422,91708,74270,91678,129306,74620,129850,92028,74558,91966,127546,128634,124026,126202,116986,121338,102906,90848,110968,121022,90736,110908,90680,110878,90652,90638,74104,102590,91e3,74044,90940,74014,90910,74174,91070,90480,110780,90424,110750,90396,90382,73916,90556,73886,90526,90296,110686,90268,90254,73822,90334,90204,90190,71136,101112,116094,70896,100988,70776,100926,70716,70686,129204,71416,101246,129202,71292,71230,127348,71550,127346,123636,123634,116212,116210,101364,101362,79296,105200,118140,79072,105080,118078,78960,105020,78904,104990,78876,78862,70384,100732,79600,70264,100670,79480,105278,79420,70174,79390,129178,70524,129466,79740,70462,79678,127290,127866,123514,124666,115962,118266,100858,113376,122232,126654,95424,113264,122172,95328,113208,122142,95280,113180,95256,113166,95244,78560,104824,117950,95968,78448,104764,95856,113468,104734,95800,78364,95772,78350,95758,70008,100542,78712,69948,96120,78652,69918,96060,78622,96030,70078,78782,96190,94912,113008,122044,94816,112952,122014,94768,112924,94744,112910,94732,94726,78192,104636,95088,78136,104606,95032,113054,95004,78094,94990,69820,78268,69790,95164,78238,95134,94560,112824,121950,94512,112796,94488,112782,94476,94470,78008,104542,94648,77980,94620,77966,94606,69726,78046,94686,94384,112732,94360,112718,94348,94342,77916,94428,77902,94414,94296,112686,94284,94278,77870,94318,94252,94246,68336,99708,68216,99646,68156,68126,68476,68414,127162,123258,115450,99834,72416,101752,116414,72304,101692,72248,101662,72220,72206,67960,99518,72568,67900,72508,67870,72478,68030,72638,80576,105840,118460,80480,105784,118430,80432,105756,80408,105742,80396,80390,72048,101564,80752,71992,101534,80696,71964,80668,71950,80654,67772,72124,67742,80828,72094,80798,114016,122552,126814,96832,113968,122524,96800,113944,122510,96784,113932,96776,113926,96772,80224,105656,118366,97120,80176,105628,97072,114076,105614,97048,80140,97036,80134,97030,71864,101470,80312,71836,97208,80284,71822,97180,80270,97166,67678,71902,80350,97246,96576,113840,122460,96544,113816,122446,96528,113804,96520,113798,96516,96514,80048,105564,96688,80024,105550,96664,113870,96652,80006,96646,71772,80092,71758,96732,80078,96718,96416,113752,122414,96400,113740,96392,113734,96388,96386,79960,105518,96472,79948,96460,79942,96454,71726,79982,96494,96336,113708,96328,113702,96324,96322,79916,96364,79910,96358,96296,113686,96292,96290,79894,96310,66936,99006,66876,66846,67006,68976,100028,68920,99998,68892,68878,66748,69052,66718,69022,73056,102072,116574,73008,102044,72984,102030,72972,72966,68792,99934,73144,68764,73116,68750,73102,66654,68830,73182,81216,106160,118620,81184,106136,118606,81168,106124,81160,106118,81156,81154,72880,101980,81328,72856,101966,81304,106190,81292,72838,81286,68700,72924,68686,81372,72910,81358,114336,122712,126894,114320,122700,114312,122694,114308,114306,81056,106072,118574,97696,81040,106060,97680,114380,106054,97672,81028,97668,81026,97666,72792,101934,81112,72780,97752,81100,72774,97740,81094,97734,68654,72814,81134,97774,114256,122668,114248,122662,114244,114242,80976,106028,97488,80968,106022,97480,114278,97476,80962,97474,72748,81004,72742,97516,80998,97510,114216,122646,114212,114210,80936,106006,97384,80932,97380,80930,97378,72726,80950,97398,114196,114194,80916,97332,80914,97330,66236,66206,67256,99166,67228,67214,66142,67294,69296,100188,69272,100174,69260,69254,67164,69340,67150,69326,73376,102232,116654,73360,102220,73352,102214,73348,73346,69208,100142,73432,102254,73420,69190,73414,67118,69230,73454,106320,118700,106312,118694,106308,106306,73296,102188,81616,106348,102182,81608,73284,81604,73282,81602,69164,73324,69158,81644,73318,81638,122792,126934,122788,122786,106280,118678,114536,106276,114532,106274,114530,73256,102166,81512,73252,98024,81508,73250,98020,81506,98018,69142,73270,81526,98038,122772,122770,106260,114484,106258,114482,73236,81460,73234,97908,81458,97906,122762,106250,114458,73226,81434,97850,66396,66382,67416,99246,67404,67398,66350,67438,69456,100268,69448,100262,69444,69442,67372,69484,67366,69478,102312,116694,102308,102306,69416,100246,73576,102326,73572,69410,73570,67350,69430,73590,118740,118738,102292,106420,102290,106418,69396,73524,69394,81780,73522,81778,118730,102282,106394,69386,73498,81722,66476,66470,67496,99286,67492,67490,66454,67510,100308,100306,67476,69556,67474,69554,116714])]);$_.micropdf417_raps=$a([$a([802,930,946,818,882,890,826,954,922,986,970,906,778,794,786,914,978,982,980,916,948,932,934,942,940,936,808,812,814,806,822,950,918,790,788,820,884,868,870,878,876,872,840,856,860,862,846,844,836,838,834,866]),$a([718,590,622,558,550,566,534,530,538,570,562,546,610,626,634,762,754,758,630,628,612,614,582,578,706,738,742,740,748,620,556,552,616,744,712,716,708,710,646,654,652,668,664,696,688,656,720,592,600,604,732,734])]);$_.micropdf417_rwids=$a([38,55,82,99]);$_.micropdf417_textencfuncs=$a(["enca","encl","encm","encp"]);$_.micropdf417_encfuncs=$a(["enct","encn","encb"]);$_.charvals=$a([new Map,new Map,new Map,new Map]);$_.alltext=new Map;for(var _2l=0,_2k=$_.micropdf417_charmaps.length-1;_2l<=_2k;_2l+=1){$_.i=_2l;$_.encs=$g($_.micropdf417_charmaps,$_.i);for(var _2o=0;_2o<=3;_2o+=1){$_.j=_2o;var _2r=$g($_.encs,$_.j);var _2s=$t(_2r);if($eq(_2s,"stringtype")){_2r=$g(_2r,0)}$p($g($_.charvals,$_.j),_2r,$_.i);$p($_.alltext,_2r,-1)}}$_.charvals=$_.charvals;$_.alltext=$_.alltext;$k[$j++]=Infinity;$k[$j++]=1;for(var _33=0;_33<928;_33++){var _34=$k[$j-1];$k[$j++]=_34*3%929}var _35=$a();$_.rsalog=_35;$_.rslog=$a(929);for(var _37=1;_37<=928;_37+=1){$p($_.rslog,$g($_.rsalog,_37),_37)}$_.rslog=$_.rslog;$_.micropdf417_rsprod=function(){var _3C=$k[$j-1];if(_3C!=0){var _3E=$k[$j-2];if(_3E!=0){var _3G=$k[--$j];var _3I=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_3G)+$g($_.rslog,_3I))%928)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.micropdf417_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _3O=0,_3P=$_.k;_3O<_3P;_3O++){$k[$j++]=0}$_.coeffs=$a();for(var _3S=1,_3R=$_.k;_3S<=_3R;_3S+=1){var _3T=$_.coeffs;$p(_3T,_3S,$g(_3T,_3S-1));$k[$j++]=$g($_.rsalog,_3S);for(var _3X=_3S-1;_3X>=1;_3X-=1){var _3Y=$_.coeffs;var _3a=$k[$j-1];$k[$j++]=_3Y;$k[$j++]=_3X;$k[$j++]=$g(_3Y,_3X);$k[$j++]=_3a;$_.micropdf417_rsprod();var _3c=$k[--$j];var _3d=$k[--$j];var _3f=$k[--$j];$p(_3f,_3d,$f(_3c+$g($_.coeffs,_3d-1))%929)}var _3g=$_.coeffs;var _3i=$k[$j-1];$k[$j++]=_3g;$k[$j++]=0;$k[$j++]=$g(_3g,0);$k[$j++]=_3i;$_.micropdf417_rsprod();var _3j=$k[--$j];var _3k=$k[--$j];var _3l=$k[--$j];$p(_3l,_3k,_3j);$j--}$_.coeffs=$G($_.coeffs,0,$_.coeffs.length-1);for(var _3q=$_.coeffs.length-1;_3q>=0;_3q-=2){var _3r=$_.coeffs;$p(_3r,_3q,929-$g(_3r,_3q))}$k[$j++]=$_.coeffs};$_.micropdf417_coeffscachemax=20;$_.micropdf417_coeffscachelimit=359;$k[$j++]=$_.micropdf417_coeffscachemax;$k[$j++]=$_.micropdf417_coeffscachelimit;bwipp_fifocache();var _3w=$k[--$j];$_.micropdf417_coeffscache=_3w;$_.micropdf417_coeffscachefetch=function(){var _3x=$k[--$j];$_.k=_3x;var _3z=$g($_.micropdf417_coeffscache,"fetch");$k[$j++]=$_.k;$k[$j++]=function(){$_.micropdf417_gencoeffs()};$k[$j++]=function(){var _40=$k[--$j];$k[$j++]=_40.length};if(_3z()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_micropdf417.globals);$_.dontdraw=false;$_.version="unset";$_.columns=0;$_.rows=0;$_.rowmult=2;$_.cca=false;$_.ccb=false;$_.raw=false;$_.parse=false;$_.parsefnc=false;$_.modunit=1;bwipp_processoptions();var _41=$k[--$j];$_.options=_41;var _42=$k[--$j];$_.barcode=_42;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.micropdf417emptyData#24035";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>5e3){$k[$j++]="bwipp.micropdf417inputTooLong#24039";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.rowmult<=0||$_.rowmult>50){$k[$j++]="bwipp.micropdf417badRowMult#24043";$k[$j++]="The row multiplier must be greater than 0 and at most 50";bwipp_raiseerror()}if($_.cca&&$_.ccb||($_.cca&&$_.raw||$_.ccb&&$_.raw)){$k[$j++]="bwipp.micropdf417ccaAndCcb#24047";$k[$j++]="Cannot combine cca, ccb and raw";bwipp_raiseerror()}if($ne($_.version,"unset")){$X($_.version,"x");var _4H=$k[--$j];if(_4H){var _4I=$k[--$j];$k[$j-1]=_4I;var _4K=$k[$j-1];var _4L=$k[$j-2];var _4M=$a(2);$k[$j++]=_4L;$k[$j++]=_4K;$as(_4M);var _4N=$k[--$j];$F(_4N,function(){var _4O=$k[$j-1];var _4P=true;if(_4O.length==0){_4P=false}var _4Q=$k[--$j];$k[$j++]=_4P;$F(_4Q,function(){var _4R=$k[--$j];if(_4R<48||_4R>57){$k[$j-1]=false}});var _4S=$k[--$j];if(!_4S){$j-=2;$k[$j++]="bwipp.micropdf417BadVersionBadRowOrColumn#24063";$k[$j++]="version must be formatted as RxC";bwipp_raiseerror()}});var _4T=$k[--$j];$_.rows=$i(_4T);var _4U=$k[--$j];$_.columns=$i(_4U)}else{$k[$j-1]="bwipp.micropdf417badVersionFormat#24069";$k[$j++]="version must be formatted as RxC";bwipp_raiseerror()}}if($_.raw||$_.cca){$_.datcws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _4g=$G($_.barcode,$_.i+1,3);$k[$j++]=_4g;$k[$j++]=false;for(var _4h=0,_4i=_4g.length;_4h<_4i;_4h++){var _4j=$g(_4g,_4h);if(_4j<48||_4j>57){$k[$j-1]=true}}var _4k=$k[--$j];if(_4k){$j--;break}var _4l=$k[--$j];$_.cw=$i(_4l);if($_.cw>928){$k[$j++]="bwipp.micropdf417badRawCodeword#24089";$k[$j++]="Raw codewords must be 0 to 928";bwipp_raiseerror()}$p($_.datcws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.micropdf417badCcaRawFormat#24096";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.datcws=$G($_.datcws,0,$_.j)}$_.encb=function(){var _4x=$k[--$j];$_.in=_4x;$_.inlen=$_.in.length;$_.out=$a(~~($_.inlen/6)*5+$_.inlen%6);for(var _54=0,_53=~~($_.inlen/6)-1;_54<=_53;_54+=1){$_.k=_54;$k[$j++]=Infinity;$q($G($_.in,$_.k*6,3));$_.msbs=$a();$k[$j++]=Infinity;$q($_.msbs);var _59=$k[--$j];var _5A=$k[--$j];var _5B=$k[--$j];$k[$j++]=$f(_59+_5A*256+_5B*65536);for(var _5C=0;_5C<3;_5C++){var _5D=$k[--$j];$k[$j++]=_5D%900;$k[$j++]=~~(_5D/900)}$_.mscs=$a();$k[$j++]=Infinity;$q($G($_.in,$_.k*6+3,3));$_.lsbs=$a();$k[$j++]=Infinity;$q($_.lsbs);var _5I=$k[--$j];var _5J=$k[--$j];var _5K=$k[--$j];$k[$j++]=$f(_5I+_5J*256+_5K*65536);for(var _5L=0;_5L<3;_5L++){var _5M=$k[--$j];$k[$j++]=_5M%900;$k[$j++]=~~(_5M/900)}$_.lscs=$a();var _5O=$g($_.lscs,0);var _5Q=$g($_.mscs,0);$p($_.out,$_.k*5+4,$f(_5O+_5Q*316)%900);var _5U=$g($_.lscs,1);var _5W=$g($_.mscs,0);var _5Y=$g($_.mscs,1);$p($_.out,$_.k*5+3,$f(~~((_5O+_5Q*316)/900)+_5U+_5W*641+_5Y*316)%900);var _5c=$g($_.lscs,2);var _5e=$g($_.mscs,0);var _5g=$g($_.mscs,1);var _5i=$g($_.mscs,2);$p($_.out,$_.k*5+2,$f(~~((~~((_5O+_5Q*316)/900)+_5U+_5W*641+_5Y*316)/900)+_5c+_5e*20+_5g*641+_5i*316)%900);var _5m=$g($_.lscs,3);var _5o=$g($_.mscs,1);var _5q=$g($_.mscs,2);$p($_.out,$_.k*5+1,$f(~~((~~((~~((_5O+_5Q*316)/900)+_5U+_5W*641+_5Y*316)/900)+_5c+_5e*20+_5g*641+_5i*316)/900)+_5m+_5o*20+_5q*641)%900);$p($_.out,$_.k*5,$f(~~((~~((~~((~~((_5O+_5Q*316)/900)+_5U+_5W*641+_5Y*316)/900)+_5c+_5e*20+_5g*641+_5i*316)/900)+_5m+_5o*20+_5q*641)/900)+$g($_.mscs,2)*20)%900)}$_.rem=$_.inlen%6;if($_.rem!=0){var _69=$_.out;var _68=$_.out.length-$_.rem;$k[$j++]=Infinity;$q($G($_.in,$_.inlen-$_.rem,$_.rem));var _67=$a();$P(_69,_68,_67)}$k[$j++]=$_.out};if($_.ccb){$_.barlen=$_.barcode.length;$_.datcws=$a(~~($_.barlen/6)*5+$_.barlen%6+2);$p($_.datcws,0,920);var _6J=$_.barlen%6==0?924:901;$p($_.datcws,1,_6J);$k[$j++]=$_.datcws;$k[$j++]=2;$k[$j++]=Infinity;$F($_.barcode);var _6M=$a();$k[$j++]=_6M;$_.encb();var _6N=$k[--$j];var _6O=$k[--$j];var _6P=$k[--$j];$P(_6P,_6O,_6N)}if(!$_.raw&&!$_.cca&&!$_.ccb){$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _6X=$k[--$j];$_.msg=_6X;$_.msglen=$_.msg.length;if($_.msglen>=9){$q($G($_.msg,0,7));var _6c=$k[--$j];$j--;var _6e=_6c==29;var _6f=$k[--$j];var _6g=$k[--$j];var _6h=$k[--$j];var _6i=$k[--$j];var _6j=$k[--$j];if(_6e&&_6f==48&&_6g==30&&_6h==62&&_6i==41&&_6j==91){$q($G($_.msg,$_.msglen-2,2));var _6n=$k[--$j];var _6o=$k[--$j];if(_6n==4&&_6o==30){if($g($_.msg,5)==53){$k[$j++]=Infinity;$k[$j++]=$_.micropdf417_m05;$q($G($_.msg,7,$_.msg.length-9));$_.msg=$a()}else{if($g($_.msg,5)==54){$k[$j++]=Infinity;$k[$j++]=$_.micropdf417_m06;$q($G($_.msg,7,$_.msg.length-9));$_.msg=$a()}}}}}$_.msglen=$_.msg.length;$k[$j++]=Infinity;for(var _71=0,_72=$_.msglen;_71<_72;_71++){$k[$j++]=0}$k[$j++]=0;$_.numdigits=$a();$k[$j++]=Infinity;for(var _74=0,_75=$_.msglen;_74<_75;_74++){$k[$j++]=0}$k[$j++]=0;$_.numtext=$a();$k[$j++]=Infinity;for(var _77=0,_78=$_.msglen;_77<_78;_77++){$k[$j++]=0}$k[$j++]=0;$_.numbytes=$a();$k[$j++]=Infinity;for(var _7A=0,_7B=$_.msglen;_7A<_7B;_7A++){$k[$j++]=0}$_.iseci=$a();for(var _7D=$_.msglen-1;_7D>=0;_7D-=1){$_.i=_7D;var _7G=$g($_.msg,$_.i);if(_7G>=48&&_7G<=57){$p($_.numdigits,$_.i,$g($_.numdigits,$_.i+1)+1)}if($has($_.alltext,$g($_.msg,$_.i))&&$g($_.numdigits,$_.i)<13){$p($_.numtext,$_.i,$g($_.numtext,$_.i+1)+1)}if($g($_.msg,$_.i)>=0&&$g($_.numtext,$_.i)<5&&$g($_.numdigits,$_.i)<13){$p($_.numbytes,$_.i,$g($_.numbytes,$_.i+1)+1)}$p($_.iseci,$_.i,$g($_.msg,$_.i)<=-1e6)}$_.numdigits=$G($_.numdigits,0,$_.msglen);$_.numtext=$G($_.numtext,0,$_.msglen);$_.numbytes=$G($_.numbytes,0,$_.msglen);$k[$j++]=Infinity;$_.seqlen=0;$_.state=$_.micropdf417_b;$_.p=0;for(;;){if($_.p==$_.msglen){break}var _86=$g($_.msg,$_.p);if($_.p==0&&(_86==$_.micropdf417_m05||_86==$_.micropdf417_m06)){if($g($_.msg,$_.p)==$_.micropdf417_m05){$k[$j++]=$_.micropdf417_m05}else{$k[$j++]=$_.micropdf417_m06}$_.p=$_.p+1;$_.seqlen=$_.seqlen+1}else{if($g($_.iseci,$_.p)){$_.eci=$g($_.msg,$_.p);$_.p=$_.p+1;$k[$j++]=$a([$_.eci]);var _8Q=$_.seqlen;var _8P=$_.eci<=-1810900?2:$_.eci<=-1000900?3:2;$_.seqlen=$f(_8Q+_8P)}else{$_.n=$g($_.numdigits,$_.p);if($_.n>=13||$_.n==$_.msglen&&$_.n>=8){$k[$j++]=$_.micropdf417_nl;$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.n));var _8d=$a();$_.state=$_.micropdf417_n;$_.p=$f($_.p+$_.n);$_.seqlen=$f($_.seqlen+1+$_.n);$k[$j++]=_8d}else{$_.t=$g($_.numtext,$_.p);if($_.t>=5){if($_.state!=$_.micropdf417_t){$k[$j++]=$_.micropdf417_tl}$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.t));var _8r=$a();$_.state=$_.micropdf417_t;$_.p=$f($_.p+$_.t);$_.seqlen=$f($_.seqlen+1+$_.t);$k[$j++]=_8r}else{$_.b=$g($_.numbytes,$_.p);if($_.b==1&&$_.state==$_.micropdf417_t){$_.p=$f($_.p+$_.b);$_.seqlen=$_.seqlen+2;$k[$j++]=$_.micropdf417_bs;$k[$j++]=$a([$g($_.msg,$_.p)])}else{if($_.b%6!=0){$k[$j++]=$_.micropdf417_bl}else{$k[$j++]=$_.micropdf417_bl6}$k[$j++]=Infinity;$q($G($_.msg,$_.p,$_.b));var _9D=$a();$_.state=$_.micropdf417_b;$_.p=$f($_.p+$_.b);$_.seqlen=$f($_.seqlen+1+$_.b);$k[$j++]=_9D}}}}}}$as($a($m()));var _9K=$k[--$j];$j--;$_.seq=_9K;$k[$j++]=Infinity;$k[$j++]=$_.micropdf417_tl;$k[$j++]=900;$k[$j++]=$_.micropdf417_bl;$k[$j++]=901;$k[$j++]=$_.micropdf417_bl6;$k[$j++]=924;$k[$j++]=$_.micropdf417_nl;$k[$j++]=902;$k[$j++]=$_.micropdf417_bs;$k[$j++]=913;$k[$j++]=$_.micropdf417_m05;$k[$j++]=916;$k[$j++]=$_.micropdf417_m06;$k[$j++]=917;$_.latchcws=$d();$_.enca=function(){var _9P=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.micropdf417_a),_9P)};$_.encl=function(){var _9T=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.micropdf417_l),_9T)};$_.encm=function(){var _9X=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.micropdf417_m),_9X)};$_.encp=function(){var _9b=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.micropdf417_p),_9b)};$_.textencfuncs=$_.micropdf417_textencfuncs;$_.addtotext=function(){var _9e=$k[--$j];$p($_.text,$_.l,_9e);$_.l=$_.l+1};$_.enct=function(){var _9h=$k[--$j];$_.in=_9h;$_.maxseqlen=$_.in.length*3+4;$k[$j++]=Infinity;for(var _9j=0;_9j<4;_9j++){$k[$j++]=$a($_.maxseqlen)}$_.seq0=$a();$k[$j++]=Infinity;for(var _9m=0;_9m<4;_9m++){$k[$j++]=$a($_.maxseqlen)}$_.seq1=$a();$_.len0=$a(4);$p($_.len0,0,0);$p($_.len0,1,0);$p($_.len0,2,0);$p($_.len0,3,0);$_.len1=$a(4);$_.curseq=$_.seq0;$_.curseqlen=$_.len0;$_.nxtseq=$_.seq1;$_.nxtseqlen=$_.len1;$_.curlen=$a([$_.micropdf417_e,$_.micropdf417_e,$_.micropdf417_e,$_.micropdf417_e]);$p($_.curlen,$_.submode,0);$F($_.in,function(){var _A2=$k[--$j];$_.char=_A2;for(;;){$_.imp=false;for(var _A3=0,_A4=$_.micropdf417_textsubmodes.length;_A3<_A4;_A3++){$_.x=$g($_.micropdf417_textsubmodes,_A3);for(var _A6=0,_A7=$_.micropdf417_textsubmodes.length;_A6<_A7;_A6++){$_.y=$g($_.micropdf417_textsubmodes,_A6);$_.cost=$f($g($_.curlen,$_.x)+$g($g($_.micropdf417_latlen,$_.x),$_.y));if($_.cost<$g($_.curlen,$_.y)){$p($_.curlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.curseq,$_.y),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$F($g($g($_.micropdf417_latseq,$_.x),$_.y),function(){$k[$j++]=$g($_.curseq,$_.y);$k[$j++]=$_.srclen;var _Ah=$k[$j-3];var _Ai=$k[$j-1];var _Aj=$k[$j-2];$j-=3;$p(_Aj,_Ai,_Ah);$_.srclen=$_.srclen+1});$p($_.curseqlen,$_.y,$_.srclen);$_.imp=true}}}if(!$_.imp){break}}$_.nxtlen=$a([$_.micropdf417_e,$_.micropdf417_e,$_.micropdf417_e,$_.micropdf417_e]);$p($_.nxtseqlen,0,0);$p($_.nxtseqlen,1,0);$p($_.nxtseqlen,2,0);$p($_.nxtseqlen,3,0);for(var _At=0,_Au=$_.micropdf417_textsubmodes.length;_At<_Au;_At++){$_.x=$g($_.micropdf417_textsubmodes,_At);for(;;){if(!$has($g($_.charvals,$_.x),$_.char)){break}$_.cost=$g($_.curlen,$_.x)+1;if($_.cost<$g($_.nxtlen,$_.x)){$p($_.nxtlen,$_.x,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.nxtseq,$_.x),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$p($g($_.nxtseq,$_.x),$_.srclen,$_.char);$p($_.nxtseqlen,$_.x,$_.srclen+1)}for(var _BV=0,_BW=$_.micropdf417_textsubmodes.length;_BV<_BW;_BV++){$_.y=$g($_.micropdf417_textsubmodes,_BV);if($ne($_.x,$_.y)){$_.cost=$f($g($_.curlen,$_.y)+$g($g($_.micropdf417_shftlen,$_.y),$_.x))+1;if($_.cost<$g($_.nxtlen,$_.y)){$p($_.nxtlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.y);if($_.srclen>0){$P($g($_.nxtseq,$_.y),0,$G($g($_.curseq,$_.y),0,$_.srclen))}var _C7=$g($_.nxtseq,$_.y);var _C6=$_.srclen;var _C5=$_.x==$_.micropdf417_a?$_.micropdf417_as:$_.micropdf417_ps;$p(_C7,_C6,_C5);$p($g($_.nxtseq,$_.y),$_.srclen+1,$_.char);$p($_.nxtseqlen,$_.y,$_.srclen+2)}}}break}}$_.curlen=$_.nxtlen;var _CH=$_.curseq;$_.curseq=$_.nxtseq;$_.nxtseq=_CH;var _CJ=$_.curseqlen;$_.curseqlen=$_.nxtseqlen;$_.nxtseqlen=_CJ});$_.minseq=$_.micropdf417_e;for(var _CL=0,_CM=$_.micropdf417_textsubmodes.length;_CL<_CM;_CL++){$_.k=$g($_.micropdf417_textsubmodes,_CL);if($g($_.curlen,$_.k)<$_.minseq){$_.minseq=$g($_.curlen,$_.k);$_.txtseq=$G($g($_.curseq,$_.k),0,$g($_.curseqlen,$_.k))}}$_.text=$a($_.minseq);$_.k=0;$_.l=0;for(;;){if($_.k>=$_.txtseq.length){break}$_.char=$g($_.txtseq,$_.k);var _Cn=$_[$g($_.textencfuncs,$_.submode)];$k[$j++]=$_.char;if(_Cn()===true){break}$_.addtotext();$_.k=$_.k+1;if($_.char==$_.micropdf417_as||$_.char==$_.micropdf417_ps){$k[$j++]=$g($_.txtseq,$_.k);if($_.char==$_.micropdf417_as){$_.enca()}else{$_.encp()}$_.addtotext();$_.k=$_.k+1}if($_.char==$_.micropdf417_al){$_.submode=$_.micropdf417_a}if($_.char==$_.micropdf417_ll){$_.submode=$_.micropdf417_l}if($_.char==$_.micropdf417_ml){$_.submode=$_.micropdf417_m}if($_.char==$_.micropdf417_pl){$_.submode=$_.micropdf417_p}}if($_.text.length%2==1){if($_.submode==$_.micropdf417_p){$k[$j++]="pad";$k[$j++]=$_.micropdf417_al;$_.encp();var _D2=$k[--$j];var _D3=$k[--$j];$_[_D3]=_D2;$_.submode=$_.micropdf417_a}else{$_.nextecitext=false;for(var _D7=$_.i+1,_D6=$_.seq.length-1;_D7<=_D6;_D7+=1){$_.ti=_D7;if($ne($t($g($_.seq,$_.ti)),"arraytype")){$_.nextecitext=false;break}if($g($g($_.seq,$_.ti),0)>-1e6){break}$_.nextecitext=true}if($_.nextecitext){if($_.submode==$_.micropdf417_a){if($has($g($_.charvals,$_.micropdf417_l),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.micropdf417_ll;$_.enca();var _DP=$k[--$j];var _DQ=$k[--$j];$_[_DQ]=_DP;$_.submode=$_.micropdf417_l}else{$k[$j++]="pad";$k[$j++]=$_.micropdf417_ml;$_.enca();var _DR=$k[--$j];var _DS=$k[--$j];$_[_DS]=_DR;$_.submode=$_.micropdf417_m}}else{if($_.submode==$_.micropdf417_m){if($has($g($_.charvals,$_.micropdf417_l),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.micropdf417_ll;$_.encm();var _Db=$k[--$j];var _Dc=$k[--$j];$_[_Dc]=_Db;$_.submode=$_.micropdf417_l}else{if($has($g($_.charvals,$_.micropdf417_p),$g($g($_.seq,$_.ti),0))){$k[$j++]="pad";$k[$j++]=$_.micropdf417_pl;$_.encm();var _Dk=$k[--$j];var _Dl=$k[--$j];$_[_Dl]=_Dk;$_.submode=$_.micropdf417_p}else{$k[$j++]="pad";$k[$j++]=$_.micropdf417_al;$_.encm();var _Dm=$k[--$j];var _Dn=$k[--$j];$_[_Dn]=_Dm;$_.submode=$_.micropdf417_a}}}else{$k[$j++]="pad";$k[$j++]=$_.micropdf417_ml;$_.encl();var _Do=$k[--$j];var _Dp=$k[--$j];$_[_Dp]=_Do;$_.submode=$_.micropdf417_m}}}else{var _Dt=$_[$g($_.textencfuncs,$_.submode)];$k[$j++]="pad";$k[$j++]=$_.micropdf417_ps;if(_Dt()===true){return true}var _Du=$k[--$j];var _Dv=$k[--$j];$_[_Dv]=_Du}}$k[$j++]=Infinity;$q($_.text);$k[$j++]=$_.pad;$_.text=$a()}$_.out=$a(~~($_.text.length/2));for(var _E2=0,_E1=$_.out.length-1;_E2<=_E1;_E2+=1){$_.k=_E2;$p($_.out,$_.k,$f($g($_.text,$_.k*2)*30+$g($_.text,$_.k*2+1)))}$k[$j++]=$_.out};$_.encn=function(){var _EC=$k[--$j];$_.in=_EC;$k[$j++]=Infinity;for(var _EF=0,_EE=$_.in.length-1;_EF<=_EE;_EF+=44){$_.k=_EF;$k[$j++]=Infinity;var _EK=$_.in.length-$_.k;$k[$j++]=1;var _EN=$_.in;var _EM=$_.k;if(_EK>44){_EK=44}var _EO=$G(_EN,_EM,_EK);for(var _EP=0,_EQ=_EO.length;_EP<_EQ;_EP++){$k[$j++]=$g(_EO,_EP)-48}$_.gmod=$a();$_.cwn=$a(15);$_.cwi=15;for(;;){$_.dv=900;$_.val=0;$_.gmulstart=true;$k[$j++]=Infinity;for(;;){if($_.gmod.length==0){break}$_.val=$f($_.val*10+$g($_.gmod,0));$_.gmod=$G($_.gmod,1,$_.gmod.length-1);if($_.val<$_.dv){if(!$_.gmulstart){$k[$j++]=0}}else{$_.gmulstart=false;$k[$j++]=~~($_.val/$_.dv)}$_.val=$_.val%$_.dv}$as($a($m()));var _Ej=$k[--$j];$j--;$_.dv=$_.val;$_.cwi=$_.cwi-1;$p($_.cwn,$_.cwi,$_.dv);$_.gmod=_Ej;if($_.gmod.length==0){break}}$q($G($_.cwn,$_.cwi,15-$_.cwi))}$as($a($m()));var _Ey=$k[--$j];$k[$j-1]=_Ey};$_.ence=function(){var _F0=$k[--$j];var _F2=-$g(_F0,0)-1e6;$k[$j++]=_F2;if(_F2<=899){var _F3=$k[--$j];var _F4=$a(2);$k[$j++]=927;$k[$j++]=_F3;$as(_F4)}else{var _F5=$k[$j-1];if(_F5<=810899){var _F6=$k[--$j];var _F7=$a(3);$k[$j++]=926;$k[$j++]=~~(_F6/900)-1;$k[$j++]=_F6%900;$as(_F7)}else{var _F8=$k[$j-1];if(_F8<=811799){var _F9=$k[--$j];var _FA=$a(2);$k[$j++]=925;$k[$j++]=_F9-810900;$as(_FA)}else{$k[$j-1]="bwipp.pdf417badECI#24539";$k[$j++]="PDF417 supports ECIs 000000 to 811799";bwipp_raiseerror()}}}};$_.encfuncs=$_.micropdf417_encfuncs;$_.addtocws=function(){var _FB=$k[--$j];$P($_.datcws,$_.j,_FB);$_.j=_FB.length+$_.j};$_.datcws=$a($_.seqlen);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.seq.length){break}$_.chars=$g($_.seq,$_.i);if($eq($t($_.chars),"arraytype")){if($g($_.chars,0)<=-1e6){$k[$j++]=$_.chars;$_.ence();$_.addtocws()}else{var _FV=$_[$g($_.encfuncs,$_.state)];$k[$j++]=$_.chars;if(_FV()===true){break}$_.addtocws()}}else{$k[$j++]=$a([$g($_.latchcws,$_.chars)]);$_.addtocws();if($_.chars==$_.micropdf417_tl){$_.state=$_.micropdf417_t;$_.submode=$_.micropdf417_a}if($_.chars==$_.micropdf417_nl){$_.state=$_.micropdf417_n}if($_.chars==$_.micropdf417_bl||$_.chars==$_.micropdf417_bl6){$_.state=$_.micropdf417_b}if($_.chars==$_.micropdf417_bs){$_.i=$_.i+1;$k[$j++]=$g($_.seq,$_.i);$_.encb();$_.addtocws()}if($_.chars==$_.micropdf417_m05){if($_.i+1<$_.seq.length){if($g($_.seq,$_.i+1)==$_.micropdf417_nl){$_.i=$_.i+1}}$_.state=$_.micropdf417_n}if($_.chars==$_.micropdf417_m06){if($_.i+1<$_.seq.length){if($g($_.seq,$_.i+1)==$_.micropdf417_tl){$_.i=$_.i+1}}$_.state=$_.micropdf417_t;$_.submode=$_.micropdf417_m}}$_.i=$_.i+1}$_.datcws=$G($_.datcws,0,$_.j)}$_.urows=$_.rows;$_.ucols=$_.columns;var _G4=$_.cca?$_.micropdf417_ccametrics:$_.micropdf417_nonccametrics;$_.metrics=_G4;$F($_.metrics,function(){var _G7=$k[--$j];$_.m=_G7;$_.c=$g($_.m,0);$_.r=$g($_.m,1);$_.k=$g($_.m,2);$_.rapl=$g($_.m,3);$_.rapc=$g($_.m,4);$_.rapr=$g($_.m,5);$_.ncws=$f($_.r*$_.c-$_.k);$_.okay=true;if($_.datcws.length>$_.ncws){$_.okay=false}if($_.urows!=0&&$_.urows!=$_.r){$_.okay=false}if($_.ucols!=0&&$_.ucols!=$_.c){$_.okay=false}if($_.okay){return true}});if(!$_.okay){$k[$j++]="bwipp.micropdf417noValidSymbol#24625";$k[$j++]="Maximum length exceeded or invalid size";bwipp_raiseerror()}$k[$j++]="coeffs";$k[$j++]=$_.k;$_.micropdf417_coeffscachefetch();var _GY=$k[--$j];var _GZ=$k[--$j];$_[_GZ]=_GY;$_.m=$_.datcws.length;$_.n=$f($_.c*$_.r-$_.k);$_.cws=$a($_.c*$_.r);$P($_.cws,0,$_.datcws);var _Gr=$_.cws;var _Gq=$_.m;var _Gp=[];for(var _Gn=0,_Go=$f($_.n-$_.m);_Gn<_Go;_Gn++){_Gp[_Gn]=900}_Gp=$a(_Gp);$P(_Gr,_Gq,_Gp);if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#24650";$k[$j++]=$G($_.cws,0,$_.n);bwipp_raiseerror()}var _Gx=$_.n;var _Gy=$_.coeffs;var _Gz=$_.k;var _H3=$G($_.cws,$_.n,$_.k);$k[$j++]=function(){var _H5=$k[--$j];$k[$j++]=$g($_.cws,_H5)};$k[$j++]=_Gx;$k[$j++]=_Gy;$k[$j++]=_Gz;$k[$j++]=_H3;$k[$j++]=929;bwipp_rsecprime();if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#24659";$k[$j++]=$G($_.cws,$_.n,$_.k);bwipp_raiseerror()}$_.cwtobits=function(){var _HD=$k[--$j];var _HF=$k[--$j];$_.v=$g($g($_.micropdf417_clusters,_HD),_HF);$k[$j++]=Infinity;for(var _HH=0;_HH<17;_HH++){$k[$j++]=0}var _HK=$R($s(17),$_.v,2);for(var _HL=0,_HM=_HK.length;_HL<_HM;_HL++){$k[$j++]=$g(_HK,_HL)-48}var _HO=$a();$k[$j++]=$G(_HO,_HO.length-17,17)};$_.raptobits=function(){var _HQ=$k[--$j];var _HS=$k[--$j];$_.v=$g($g($_.micropdf417_raps,_HQ),_HS);$k[$j++]=Infinity;for(var _HU=0;_HU<10;_HU++){$k[$j++]=0}var _HX=$R($s(10),$_.v,2);for(var _HY=0,_HZ=_HX.length;_HY<_HZ;_HY++){$k[$j++]=$g(_HX,_HY)-48}var _Hb=$a();$k[$j++]=$G(_Hb,_Hb.length-10,10)};$_.rwid=$g($_.micropdf417_rwids,$_.c-1);if($_.c==3&&$_.cca){$_.rwid=72}$k[$j++]=$_.rwid*$_.r;try{var _Hj=$k[--$j];$k[$j++]=$a(_Hj);var _Hl=false}catch(e){$k[$j++]=true}if(_Hl){$k[$j-1]="bwipp.micropdf417PixsTooLarge#24689";$k[$j++]="The resulting symbol exceeds the implementation maximum array size";bwipp_raiseerror()}var _Hm=$k[--$j];$_.pixs=_Hm;for(var _Hp=0,_Ho=$_.r-1;_Hp<=_Ho;_Hp+=1){$_.i=_Hp;$_.clst=($f($_.i+$_.rapl)-1)%3;var _JK=$_.pixs;var _JJ=$_.i*$_.rwid;$k[$j++]=Infinity;if($_.c==1){$k[$j++]=($f($_.i+$_.rapl)-1)%52;$k[$j++]=0;$_.raptobits();var _Hy=$k[--$j];$q(_Hy);$k[$j++]=$g($_.cws,$_.i);$k[$j++]=$_.clst;$_.cwtobits();var _I3=$k[--$j];$q(_I3);$k[$j++]=($f($_.i+$_.rapr)-1)%52;$k[$j++]=0;$_.raptobits();var _I6=$k[--$j];$q(_I6)}if($_.c==2){$k[$j++]=($f($_.i+$_.rapl)-1)%52;$k[$j++]=0;$_.raptobits();var _IA=$k[--$j];$q(_IA);$k[$j++]=$g($_.cws,$_.i*2);$k[$j++]=$_.clst;$_.cwtobits();var _IF=$k[--$j];$q(_IF);$k[$j++]=$g($_.cws,$_.i*2+1);$k[$j++]=$_.clst;$_.cwtobits();var _IK=$k[--$j];$q(_IK);$k[$j++]=($f($_.i+$_.rapr)-1)%52;$k[$j++]=0;$_.raptobits();var _IN=$k[--$j];$q(_IN)}if($_.c==3){if(!$_.cca){$k[$j++]=($f($_.i+$_.rapl)-1)%52;$k[$j++]=0;$_.raptobits();var _IS=$k[--$j];$q(_IS)}$k[$j++]=$g($_.cws,$_.i*3);$k[$j++]=$_.clst;$_.cwtobits();var _IX=$k[--$j];$q(_IX);$k[$j++]=($f($_.i+$_.rapc)-1)%52;$k[$j++]=1;$_.raptobits();var _Ia=$k[--$j];$q(_Ia);$k[$j++]=$g($_.cws,$_.i*3+1);$k[$j++]=$_.clst;$_.cwtobits();var _If=$k[--$j];$q(_If);$k[$j++]=$g($_.cws,$_.i*3+2);$k[$j++]=$_.clst;$_.cwtobits();var _Ik=$k[--$j];$q(_Ik);$k[$j++]=($f($_.i+$_.rapr)-1)%52;$k[$j++]=0;$_.raptobits();var _In=$k[--$j];$q(_In)}if($_.c==4){$k[$j++]=($f($_.i+$_.rapl)-1)%52;$k[$j++]=0;$_.raptobits();var _Ir=$k[--$j];$q(_Ir);$k[$j++]=$g($_.cws,$_.i*4);$k[$j++]=$_.clst;$_.cwtobits();var _Iw=$k[--$j];$q(_Iw);$k[$j++]=$g($_.cws,$_.i*4+1);$k[$j++]=$_.clst;$_.cwtobits();var _J1=$k[--$j];$q(_J1);$k[$j++]=($f($_.i+$_.rapc)-1)%52;$k[$j++]=1;$_.raptobits();var _J4=$k[--$j];$q(_J4);$k[$j++]=$g($_.cws,$_.i*4+2);$k[$j++]=$_.clst;$_.cwtobits();var _J9=$k[--$j];$q(_J9);$k[$j++]=$g($_.cws,$_.i*4+3);$k[$j++]=$_.clst;$_.cwtobits();var _JE=$k[--$j];$q(_JE);$k[$j++]=($f($_.i+$_.rapr)-1)%52;$k[$j++]=0;$_.raptobits();var _JH=$k[--$j];$q(_JH)}$k[$j++]=1;var _JI=$a();$P(_JK,_JJ,_JI)}$k[$j++]=Infinity;var _JL=$_.pixs;var _JM=$_.rwid;var _JN=$_.r;var _JO=$_.rowmult;var _JT=[];for(var _JQ=0,_JR=$_.r;_JQ<_JR;_JQ++){_JT[_JQ]=$_.rowmult}_JT=$a(_JT);$k[$j++]="ren";$k[$j++]="renmatrix";$k[$j++]="pixs";$k[$j++]=_JL;$k[$j++]="pixx";$k[$j++]=_JM;$k[$j++]="pixy";$k[$j++]=_JN*_JO;$k[$j++]="rowmult";$k[$j++]=_JT;$k[$j++]="height";$k[$j++]=$_.r*$_.rowmult/72;$k[$j++]="width";$k[$j++]=$_.rwid/72;$k[$j++]="borderleft";$k[$j++]=1;$k[$j++]="borderright";$k[$j++]=1;$k[$j++]="bordertop";$k[$j++]=1;$k[$j++]="borderbottom";$k[$j++]=1;$k[$j++]="opt";$k[$j++]=$_.options;var _JY=$d();$k[$j++]=_JY;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_datamatrix(){if(!bwipp_datamatrix.globals){var $__=$_;$_=bwipp_datamatrix.globals={};$_.datamatrix_metrics=$a([$a([10,10,1,1,5,1,0]),$a([12,12,1,1,7,1,0]),$a([14,14,1,1,10,1,0]),$a([16,16,1,1,12,1,0]),$a([18,18,1,1,14,1,0]),$a([20,20,1,1,18,1,0]),$a([22,22,1,1,20,1,0]),$a([24,24,1,1,24,1,0]),$a([26,26,1,1,28,1,0]),$a([32,32,2,2,36,1,0]),$a([36,36,2,2,42,1,0]),$a([40,40,2,2,48,1,0]),$a([44,44,2,2,56,1,0]),$a([48,48,2,2,68,1,0]),$a([52,52,2,2,84,2,0]),$a([64,64,4,4,112,2,0]),$a([72,72,4,4,144,4,0]),$a([80,80,4,4,192,4,0]),$a([88,88,4,4,224,4,0]),$a([96,96,4,4,272,4,0]),$a([104,104,4,4,336,6,0]),$a([120,120,6,6,408,6,0]),$a([132,132,6,6,496,8,0]),$a([144,144,6,6,620,10,0]),$a([8,18,1,1,7,1,0]),$a([8,32,1,2,11,1,0]),$a([8,48,1,2,15,1,1]),$a([8,64,1,4,18,1,1]),$a([8,80,1,4,22,1,1]),$a([8,96,1,4,28,1,1]),$a([8,120,1,6,32,1,1]),$a([8,144,1,6,36,1,1]),$a([12,26,1,1,14,1,0]),$a([12,36,1,2,18,1,0]),$a([12,64,1,4,27,1,1]),$a([12,88,1,4,36,1,1]),$a([16,36,1,2,24,1,0]),$a([16,48,1,2,28,1,0]),$a([16,64,1,4,36,1,1]),$a([20,36,1,2,28,1,1]),$a([20,44,1,2,34,1,1]),$a([20,64,1,4,42,1,1]),$a([22,48,1,2,38,1,1]),$a([24,48,1,2,41,1,1]),$a([24,64,1,4,46,1,1]),$a([26,40,1,2,38,1,1]),$a([26,48,1,2,42,1,1]),$a([26,64,1,4,50,1,1])]);$_.datamatrix_fnc1=-1;$_.datamatrix_prog=-2;$_.datamatrix_m05=-3;$_.datamatrix_m06=-4;$_.datamatrix_lC=-5;$_.datamatrix_lB=-6;$_.datamatrix_lX=-7;$_.datamatrix_lT=-8;$_.datamatrix_lE=-9;$_.datamatrix_unl=-10;$_.datamatrix_sapp=-11;$_.datamatrix_usft=-12;$_.datamatrix_sft1=-13;$_.datamatrix_sft2=-14;$_.datamatrix_sft3=-15;$_.datamatrix_eci=-16;$_.datamatrix_pad=-17;$_.datamatrix_unlcw=254;$_.datamatrix_A=0;$_.datamatrix_C=1;$_.datamatrix_T=2;$_.datamatrix_X=3;$_.datamatrix_E=4;$_.datamatrix_B=5;$_.datamatrix_encfuncs=$a(["encA","encCTX","encCTX","encCTX","encE","encB"]);var _14=$a([function(){var _o=$k[$j-1];var _p=$k[$j-2];$k[$j++]=_p-2;$k[$j++]=_o-2},function(){var _q=$k[$j-1];var _r=$k[$j-2];$k[$j++]=_r-2;$k[$j++]=_q-1},function(){var _s=$k[$j-1];var _t=$k[$j-2];$k[$j++]=_t-1;$k[$j++]=_s-2},function(){var _u=$k[$j-1];var _v=$k[$j-2];$k[$j++]=_v-1;$k[$j++]=_u-1},function(){var _w=$k[$j-1];var _x=$k[$j-2];$k[$j++]=_x-1;$k[$j++]=_w},function(){var _y=$k[$j-1];var _z=$k[$j-2];$k[$j++]=_z;$k[$j++]=_y-2},function(){var _10=$k[$j-1];var _11=$k[$j-2];$k[$j++]=_11;$k[$j++]=_10-1},function(){var _12=$k[$j-1];var _13=$k[$j-2];$k[$j++]=_13;$k[$j++]=_12}]);$_.datamatrix_dmn=_14;var _1D=$a([function(){$k[$j++]=$_.mrows-1;$k[$j++]=0},function(){$k[$j++]=$_.mrows-1;$k[$j++]=1},function(){$k[$j++]=$_.mrows-1;$k[$j++]=2},function(){$k[$j++]=0;$k[$j++]=$_.mcols-2},function(){$k[$j++]=0;$k[$j++]=$_.mcols-1},function(){$k[$j++]=1;$k[$j++]=$_.mcols-1},function(){$k[$j++]=2;$k[$j++]=$_.mcols-1},function(){$k[$j++]=3;$k[$j++]=$_.mcols-1}]);$_.datamatrix_dmc1=_1D;var _1M=$a([function(){$k[$j++]=$_.mrows-3;$k[$j++]=0},function(){$k[$j++]=$_.mrows-2;$k[$j++]=0},function(){$k[$j++]=$_.mrows-1;$k[$j++]=0},function(){$k[$j++]=0;$k[$j++]=$_.mcols-4},function(){$k[$j++]=0;$k[$j++]=$_.mcols-3},function(){$k[$j++]=0;$k[$j++]=$_.mcols-2},function(){$k[$j++]=0;$k[$j++]=$_.mcols-1},function(){$k[$j++]=1;$k[$j++]=$_.mcols-1}]);$_.datamatrix_dmc2=_1M;var _1V=$a([function(){$k[$j++]=$_.mrows-3;$k[$j++]=0},function(){$k[$j++]=$_.mrows-2;$k[$j++]=0},function(){$k[$j++]=$_.mrows-1;$k[$j++]=0},function(){$k[$j++]=0;$k[$j++]=$_.mcols-2},function(){$k[$j++]=0;$k[$j++]=$_.mcols-1},function(){$k[$j++]=1;$k[$j++]=$_.mcols-1},function(){$k[$j++]=2;$k[$j++]=$_.mcols-1},function(){$k[$j++]=3;$k[$j++]=$_.mcols-1}]);$_.datamatrix_dmc3=_1V;var _1f=$a([function(){$k[$j++]=$_.mrows-1;$k[$j++]=0},function(){$k[$j++]=$_.mrows-1;$k[$j++]=$_.mcols-1},function(){$k[$j++]=0;$k[$j++]=$_.mcols-3},function(){$k[$j++]=0;$k[$j++]=$_.mcols-2},function(){$k[$j++]=0;$k[$j++]=$_.mcols-1},function(){$k[$j++]=1;$k[$j++]=$_.mcols-3},function(){$k[$j++]=1;$k[$j++]=$_.mcols-2},function(){$k[$j++]=1;$k[$j++]=$_.mcols-1}]);$_.datamatrix_dmc4=_1f;$k[$j++]=Infinity;for(var _1g=0;_1g<=128;_1g+=1){$k[$j++]=_1g;$k[$j++]=_1g+1}$k[$j++]=$_.datamatrix_pad;$k[$j++]=129;for(var _1h=0;_1h<=99;_1h+=1){var _1j=$R($s(2),_1h,10);var _1l=$Z($s(2),"00");$P(_1l,2-_1j.length,_1j);$k[$j++]=_1l;$k[$j++]=_1h+130}var _1m=$a([$_.datamatrix_lC,$_.datamatrix_lB,$_.datamatrix_fnc1,$_.datamatrix_sapp,$_.datamatrix_prog,$_.datamatrix_usft,$_.datamatrix_m05,$_.datamatrix_m06,$_.datamatrix_lX,$_.datamatrix_lT,$_.datamatrix_lE,$_.datamatrix_eci]);$k[$j++]=229;for(var _1n=0,_1o=_1m.length;_1n<_1o;_1n++){var _1q=$k[--$j];var _1r=_1q+1;$k[$j++]=$g(_1m,_1n);$k[$j++]=_1r;$k[$j++]=_1r}$j--;$_.Avals=$d();$k[$j++]=Infinity;var _1s=$_.Avals;for(var _1x=_1s.size,_1w=_1s.keys(),_1v=0;_1v<_1x;_1v++){var _1t=_1w.next().value;var _1u=_1s.get(_1t);$k[$j++]=_1t;$k[$j++]=Infinity;$k[$j++]=_1u;var _20=$a();$k[$j++]=_20}$_.Avals=$d();$k[$j++]=Infinity;$k[$j++]=$_.datamatrix_sft1;$k[$j++]=0;$k[$j++]=$_.datamatrix_sft2;$k[$j++]=1;$k[$j++]=$_.datamatrix_sft3;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _21=48;_21<=57;_21+=1){$k[$j++]=_21;$k[$j++]=_21-44}for(var _22=65;_22<=90;_22+=1){$k[$j++]=_22;$k[$j++]=_22-51}var _23=$d();$_.CNvals=_23;$k[$j++]=Infinity;for(var _24=0;_24<=31;_24+=1){$k[$j++]=_24;$k[$j++]=_24}var _25=$d();$_.C1vals=_25;$k[$j++]=Infinity;for(var _26=33;_26<=47;_26+=1){$k[$j++]=_26;$k[$j++]=_26-33}for(var _27=58;_27<=64;_27+=1){$k[$j++]=_27;$k[$j++]=_27-43}for(var _28=91;_28<=95;_28+=1){$k[$j++]=_28;$k[$j++]=_28-69}$k[$j++]=$_.datamatrix_fnc1;$k[$j++]=27;$k[$j++]=$_.datamatrix_usft;$k[$j++]=30;var _29=$d();$_.C2vals=_29;$k[$j++]=Infinity;for(var _2A=96;_2A<=127;_2A+=1){$k[$j++]=_2A;$k[$j++]=_2A-96}var _2B=$d();$_.C3vals=_2B;$k[$j++]=Infinity;var _2C=$_.CNvals;for(var _2H=_2C.size,_2G=_2C.keys(),_2F=0;_2F<_2H;_2F++){var _2D=_2G.next().value;var _2E=_2C.get(_2D);$k[$j++]=_2D;$k[$j++]=Infinity;$k[$j++]=_2E;var _2K=$a();$k[$j++]=_2K}var _2L=$_.C1vals;for(var _2Q=_2L.size,_2P=_2L.keys(),_2O=0;_2O<_2Q;_2O++){var _2M=_2P.next().value;var _2N=_2L.get(_2M);$k[$j++]=_2M;$k[$j++]=Infinity;$k[$j++]=$g($_.CNvals,$_.datamatrix_sft1);$k[$j++]=_2N;var _2V=$a();$k[$j++]=_2V}var _2W=$_.C2vals;for(var _2b=_2W.size,_2a=_2W.keys(),_2Z=0;_2Z<_2b;_2Z++){var _2X=_2a.next().value;var _2Y=_2W.get(_2X);$k[$j++]=_2X;$k[$j++]=Infinity;$k[$j++]=$g($_.CNvals,$_.datamatrix_sft2);$k[$j++]=_2Y;var _2g=$a();$k[$j++]=_2g}var _2h=$_.C3vals;for(var _2m=_2h.size,_2l=_2h.keys(),_2k=0;_2k<_2m;_2k++){var _2i=_2l.next().value;var _2j=_2h.get(_2i);$k[$j++]=_2i;$k[$j++]=Infinity;$k[$j++]=$g($_.CNvals,$_.datamatrix_sft3);$k[$j++]=_2j;var _2r=$a();$k[$j++]=_2r}$_.Cvals=$d();$k[$j++]=Infinity;$k[$j++]=$_.datamatrix_sft1;$k[$j++]=0;$k[$j++]=$_.datamatrix_sft2;$k[$j++]=1;$k[$j++]=$_.datamatrix_sft3;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _2s=48;_2s<=57;_2s+=1){$k[$j++]=_2s;$k[$j++]=_2s-44}for(var _2t=97;_2t<=122;_2t+=1){$k[$j++]=_2t;$k[$j++]=_2t-83}var _2u=$d();$_.TNvals=_2u;$k[$j++]=Infinity;for(var _2v=0;_2v<=31;_2v+=1){$k[$j++]=_2v;$k[$j++]=_2v}var _2w=$d();$_.T1vals=_2w;$k[$j++]=Infinity;for(var _2x=33;_2x<=47;_2x+=1){$k[$j++]=_2x;$k[$j++]=_2x-33}for(var _2y=58;_2y<=64;_2y+=1){$k[$j++]=_2y;$k[$j++]=_2y-43}for(var _2z=91;_2z<=95;_2z+=1){$k[$j++]=_2z;$k[$j++]=_2z-69}$k[$j++]=$_.datamatrix_fnc1;$k[$j++]=27;$k[$j++]=$_.datamatrix_usft;$k[$j++]=30;var _30=$d();$_.T2vals=_30;$k[$j++]=Infinity;$k[$j++]=96;$k[$j++]=0;for(var _31=65;_31<=90;_31+=1){$k[$j++]=_31;$k[$j++]=_31-64}for(var _32=123;_32<=127;_32+=1){$k[$j++]=_32;$k[$j++]=_32-96}var _33=$d();$_.T3vals=_33;$k[$j++]=Infinity;var _34=$_.TNvals;for(var _39=_34.size,_38=_34.keys(),_37=0;_37<_39;_37++){var _35=_38.next().value;var _36=_34.get(_35);$k[$j++]=_35;$k[$j++]=Infinity;$k[$j++]=_36;var _3C=$a();$k[$j++]=_3C}var _3D=$_.T1vals;for(var _3I=_3D.size,_3H=_3D.keys(),_3G=0;_3G<_3I;_3G++){var _3E=_3H.next().value;var _3F=_3D.get(_3E);$k[$j++]=_3E;$k[$j++]=Infinity;$k[$j++]=$g($_.TNvals,$_.datamatrix_sft1);$k[$j++]=_3F;var _3N=$a();$k[$j++]=_3N}var _3O=$_.T2vals;for(var _3T=_3O.size,_3S=_3O.keys(),_3R=0;_3R<_3T;_3R++){var _3P=_3S.next().value;var _3Q=_3O.get(_3P);$k[$j++]=_3P;$k[$j++]=Infinity;$k[$j++]=$g($_.TNvals,$_.datamatrix_sft2);$k[$j++]=_3Q;var _3Y=$a();$k[$j++]=_3Y}var _3Z=$_.T3vals;for(var _3e=_3Z.size,_3d=_3Z.keys(),_3c=0;_3c<_3e;_3c++){var _3a=_3d.next().value;var _3b=_3Z.get(_3a);$k[$j++]=_3a;$k[$j++]=Infinity;$k[$j++]=$g($_.TNvals,$_.datamatrix_sft3);$k[$j++]=_3b;var _3j=$a();$k[$j++]=_3j}$_.Tvals=$d();for(var _3k=128;_3k<=255;_3k+=1){$_.i=_3k;var _3u=$_.Avals;var _3t=$_.i;$k[$j++]=Infinity;$q($g($_.Avals,$_.datamatrix_usft));$q($g($_.Avals,$_.i-128));var _3s=$a();$p(_3u,_3t,_3s);var _44=$_.Cvals;var _43=$_.i;$k[$j++]=Infinity;$q($g($_.Cvals,$_.datamatrix_usft));$q($g($_.Cvals,$_.i-128));var _42=$a();$p(_44,_43,_42);var _4E=$_.Tvals;var _4D=$_.i;$k[$j++]=Infinity;$q($g($_.Tvals,$_.datamatrix_usft));$q($g($_.Tvals,$_.i-128));var _4C=$a();$p(_4E,_4D,_4C)}$_.Avals=$_.Avals;$_.Cvals=$_.Cvals;$_.Tvals=$_.Tvals;$k[$j++]=Infinity;$k[$j++]=13;$k[$j++]=0;$k[$j++]=42;$k[$j++]=1;$k[$j++]=62;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _4I=48;_4I<=57;_4I+=1){$k[$j++]=_4I;$k[$j++]=_4I-44}for(var _4J=65;_4J<=90;_4J+=1){$k[$j++]=_4J;$k[$j++]=_4J-51}var _4K=$d();$_.Xvals=_4K;$k[$j++]=Infinity;var _4L=$_.Xvals;for(var _4Q=_4L.size,_4P=_4L.keys(),_4O=0;_4O<_4Q;_4O++){var _4M=_4P.next().value;var _4N=_4L.get(_4M);$k[$j++]=_4M;$k[$j++]=Infinity;$k[$j++]=_4N;var _4T=$a();$k[$j++]=_4T}var _4U=$d();$_.Xvals=_4U;$k[$j++]=Infinity;for(var _4V=64;_4V<=94;_4V+=1){$k[$j++]=_4V;$k[$j++]=_4V-64}$k[$j++]=$_.datamatrix_unl;$k[$j++]=31;for(var _4W=32;_4W<=63;_4W+=1){$k[$j++]=_4W;$k[$j++]=_4W}var _4X=$d();$_.Evals=_4X;$k[$j++]=Infinity;var _4Y=$_.Evals;for(var _4d=_4Y.size,_4c=_4Y.keys(),_4b=0;_4b<_4d;_4b++){var _4Z=_4c.next().value;var _4a=_4Y.get(_4Z);$k[$j++]=_4Z;$k[$j++]=Infinity;$k[$j++]=_4a;var _4g=$a();$k[$j++]=_4g}var _4h=$d();$_.Evals=_4h;$k[$j++]=Infinity;for(var _4i=0;_4i<=255;_4i+=1){$k[$j++]=_4i;$k[$j++]=_4i}var _4j=$d();$_.Bvals=_4j;$k[$j++]=Infinity;var _4k=$_.Bvals;for(var _4p=_4k.size,_4o=_4k.keys(),_4n=0;_4n<_4p;_4n++){var _4l=_4o.next().value;var _4m=_4k.get(_4l);$k[$j++]=_4l;$k[$j++]=Infinity;$k[$j++]=_4m;var _4s=$a();$k[$j++]=_4s}var _4t=$d();$_.Bvals=_4t;$_.encvals=$a([$_.Avals,$_.Cvals,$_.Tvals,$_.Xvals,$_.Evals,$_.Bvals]);$k[$j++]=Infinity;$k[$j++]=1;for(var _51=0;_51<255;_51++){var _52=$k[$j-1];var _53=_52*2;$k[$j++]=_53;if(_53>=256){var _54=$k[--$j];$k[$j++]=_54^301}}var _55=$a();$_.rsalog=_55;$_.rslog=$a(256);for(var _57=1;_57<=255;_57+=1){$p($_.rslog,$g($_.rsalog,_57),_57)}$_.rslog=$_.rslog;$_.datamatrix_rsprod=function(){var _5C=$k[$j-1];if(_5C!=0){var _5E=$k[$j-2];if(_5E!=0){var _5G=$k[--$j];var _5I=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_5G)+$g($_.rslog,_5I))%255)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.datamatrix_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _5O=0,_5P=$_.k;_5O<_5P;_5O++){$k[$j++]=0}$_.coeffs=$a();for(var _5S=1,_5R=$_.k;_5S<=_5R;_5S+=1){var _5T=$_.coeffs;$p(_5T,_5S,$g(_5T,_5S-1));$k[$j++]=$g($_.rsalog,_5S);for(var _5X=_5S-1;_5X>=1;_5X-=1){var _5Y=$_.coeffs;var _5a=$k[$j-1];$k[$j++]=_5Y;$k[$j++]=_5X;$k[$j++]=$g(_5Y,_5X);$k[$j++]=_5a;$_.datamatrix_rsprod();var _5c=$k[--$j];var _5d=$k[--$j];var _5f=$k[--$j];$p(_5f,_5d,$xo(_5c,$g($_.coeffs,_5d-1)))}var _5g=$_.coeffs;var _5i=$k[$j-1];$k[$j++]=_5g;$k[$j++]=0;$k[$j++]=$g(_5g,0);$k[$j++]=_5i;$_.datamatrix_rsprod();var _5j=$k[--$j];var _5k=$k[--$j];var _5l=$k[--$j];$p(_5l,_5k,_5j);$j--}$k[$j++]=$G($_.coeffs,0,$_.coeffs.length-1)};$_.datamatrix_coeffscachemax=25;$_.datamatrix_coeffscachelimit=766;$k[$j++]=$_.datamatrix_coeffscachemax;$k[$j++]=$_.datamatrix_coeffscachelimit;bwipp_fifocache();var _5r=$k[--$j];$_.datamatrix_coeffscache=_5r;$_.datamatrix_coeffscachefetch=function(){var _5s=$k[--$j];$_.k=_5s;var _5u=$g($_.datamatrix_coeffscache,"fetch");$k[$j++]=$_.k;$k[$j++]=function(){$_.datamatrix_gencoeffs()};$k[$j++]=function(){var _5v=$k[--$j];$k[$j++]=_5v.length};if(_5u()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_datamatrix.globals);$_.dontdraw=false;$_.columns=0;$_.rows=0;$_.format="unset";$_.version="unset";$_.parse=false;$_.parsefnc=false;$_.c40headerlength=-1;$_.raw=false;$_.dmre=false;bwipp_processoptions();var _5w=$k[--$j];$_.options=_5w;var _5x=$k[--$j];$_.barcode=_5x;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.barcode.length==0){$k[$j++]="bwipp.datamatrixEmptyData#25160";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>15e3){$k[$j++]="bwipp.datamatrixinputTooLong#25164";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($ne($_.version,"unset")&&($_.rows!=0||$_.columns!=0)){$k[$j++]="bwipp.datamatrixVersionRowsCols#25168";$k[$j++]="rows and columns must not be given if version is specified";bwipp_raiseerror()}if($ne($_.version,"unset")){$X($_.version,"x");var _67=$k[--$j];if(_67){var _68=$k[--$j];$k[$j-1]=_68;var _6A=$k[$j-1];var _6B=$k[$j-2];var _6C=$a(2);$k[$j++]=_6B;$k[$j++]=_6A;$as(_6C);var _6D=$k[--$j];$F(_6D,function(){var _6E=$k[$j-1];var _6F=true;if(_6E.length==0){_6F=false}var _6G=$k[--$j];$k[$j++]=_6F;$F(_6G,function(){var _6H=$k[--$j];if(_6H<48||_6H>57){$k[$j-1]=false}});var _6I=$k[--$j];if(!_6I){$j-=2;$k[$j++]="bwipp.datamatrixVersionBadRowOrColumn#25184";$k[$j++]="version must be formatted as RxC";bwipp_raiseerror()}});var _6J=$k[--$j];$_.rows=$i(_6J);var _6K=$k[--$j];$_.columns=$i(_6K);var _6N=$_.rows==$_.columns?"square":"rectangle";$_.format=_6N}else{$k[$j-1]="bwipp.datamatrixVersionFormat#25191";$k[$j++]="version must be formatted as RxC";bwipp_raiseerror()}}else{if($eq($_.format,"unset")){var _6R=$_.dmre?"rectangle":"square";$_.format=_6R}}if($ne($_.format,"square")&&$ne($_.format,"rectangle")){$k[$j++]="bwipp.datamatrixInvalidFormat#25200";$k[$j++]="The format must be either square or rectangle";bwipp_raiseerror()}$_.urows=$_.rows;$_.ucols=$_.columns;$k[$j++]=Infinity;for(var _6X=0,_6Y=$_.datamatrix_metrics.length;_6X<_6Y;_6X++){$_.m=$g($_.datamatrix_metrics,_6X);$_.rows=$g($_.m,0);$_.cols=$g($_.m,1);$_.regh=$g($_.m,2);$_.regv=$g($_.m,3);$_.rscw=$g($_.m,4);$_.rsbl=$g($_.m,5);$_.doly=$g($_.m,6);$_.mrows=$f($_.rows-2*$_.regh);$_.mcols=$f($_.cols-2*$_.regv);$_.ncws=$f(~~($_.mrows*$_.mcols/8)-$_.rscw);$_.okay=true;if($_.urows!=0&&$_.urows!=$_.rows){$_.okay=false}if($_.ucols!=0&&$_.ucols!=$_.cols){$_.okay=false}if($eq($_.format,"square")&&$ne($_.rows,$_.cols)){$_.okay=false}if($eq($_.format,"rectangle")&&$eq($_.rows,$_.cols)){$_.okay=false}if(!$_.dmre&&$_.doly==1){$_.okay=false}if($_.okay){$k[$j++]=$_.ncws}}$as($a($m()));var _7D=$k[--$j];$_.fullcws=_7D;$k[$j-1]=Infinity;for(var _7E=0;_7E<1558;_7E++){$k[$j++]=1e4}$_.numremcws=$a();$F($_.fullcws,function(){var _7H=$k[--$j];$p($_.numremcws,_7H-1,1)});for(var _7I=1556;_7I>=0;_7I-=1){$_.i=_7I;if($g($_.numremcws,$_.i)!=1){$p($_.numremcws,$_.i,$g($_.numremcws,$_.i+1)+1)}}$_.getnumremcws=function(){var _7R=$k[$j-1];if(_7R>=$_.numremcws.length){$k[$j-1]="bwipp.datamatrixTooMuchData#25243";$k[$j++]="The input data exceeds the symbol capacity";bwipp_raiseerror()}var _7U=$k[--$j];$k[$j++]=$g($_.numremcws,_7U)};if($_.raw){$_.cws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _7g=$G($_.barcode,$_.i+1,3);$k[$j++]=_7g;$k[$j++]=false;for(var _7h=0,_7i=_7g.length;_7h<_7i;_7h++){var _7j=$g(_7g,_7h);if(_7j<48||_7j>57){$k[$j-1]=true}}var _7k=$k[--$j];if(_7k){$j--;break}var _7l=$k[--$j];$_.cw=$i(_7l);if($_.cw>255){$k[$j++]="bwipp.datamatrixBadRawCodeword#25260";$k[$j++]="Raw codewords must be 0 to 255";bwipp_raiseerror()}$p($_.cws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.datamatrixBadRawFormat#25267";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.cws=$G($_.cws,0,$_.j)}else{$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true],["FNC1",$_.datamatrix_fnc1],["PROG",$_.datamatrix_prog]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _81=$k[--$j];$_.msg=_81;$_.msglen=$_.msg.length;if($_.msglen>=9){$q($G($_.msg,0,7));var _86=$k[--$j];$j--;var _88=_86==29;var _89=$k[--$j];var _8A=$k[--$j];var _8B=$k[--$j];var _8C=$k[--$j];var _8D=$k[--$j];if(_88&&_89==48&&_8A==30&&_8B==62&&_8C==41&&_8D==91){$q($G($_.msg,$_.msglen-2,2));var _8H=$k[--$j];var _8I=$k[--$j];if(_8H==4&&_8I==30){if($g($_.msg,5)==53){$k[$j++]=Infinity;$k[$j++]=$_.datamatrix_m05;$q($G($_.msg,7,$_.msg.length-9));$_.msg=$a()}else{if($g($_.msg,5)==54){$k[$j++]=Infinity;$k[$j++]=$_.datamatrix_m06;$q($G($_.msg,7,$_.msg.length-9));$_.msg=$a()}}}}}$_.msglen=$_.msg.length;if($_.c40headerlength>$_.msglen){$_.c40headerlength=$_.msglen}$k[$j++]=Infinity;for(var _8Y=0,_8Z=$_.msglen;_8Y<_8Z;_8Y++){$k[$j++]=0}$k[$j++]=0;$_.numD=$a();$k[$j++]=Infinity;for(var _8b=0,_8c=$_.msglen;_8b<_8c;_8b++){$k[$j++]=0}$k[$j++]=9999;$_.nextXterm=$a();$k[$j++]=Infinity;for(var _8e=0,_8f=$_.msglen;_8e<_8f;_8e++){$k[$j++]=0}$k[$j++]=9999;$_.nextNonX=$a();$k[$j++]=Infinity;for(var _8h=0,_8i=$_.msglen;_8h<_8i;_8h++){$k[$j++]=false}$k[$j++]=false;$_.isECI=$a();for(var _8k=$_.msglen-1;_8k>=0;_8k-=1){$_.i=_8k;$_.barchar=$g($_.msg,$_.i);if($_.barchar>=48&&$_.barchar<=57){$p($_.numD,$_.i,$g($_.numD,$_.i+1)+1)}if($_.barchar==13||$_.barchar==42||$_.barchar==62){$p($_.nextXterm,$_.i,0)}else{$p($_.nextXterm,$_.i,$g($_.nextXterm,$_.i+1)+1)}if(!$has($_.Xvals,$_.barchar)){$p($_.nextNonX,$_.i,0)}else{$p($_.nextNonX,$_.i,$g($_.nextNonX,$_.i+1)+1)}$p($_.isECI,$_.i,$_.barchar<=-1e6)}$k[$j++]=Infinity;var _9I=$_.nextXterm;for(var _9J=0,_9K=_9I.length;_9J<_9K;_9J++){var _9L=$g(_9I,_9J);$k[$j++]=_9L;if(_9L>1e4){$k[$j-1]=1e4}}$_.nextXterm=$a();$k[$j++]=Infinity;var _9M=$_.nextNonX;for(var _9N=0,_9O=_9M.length;_9N<_9O;_9N++){var _9P=$g(_9M,_9N);$k[$j++]=_9P;if(_9P>1e4){$k[$j-1]=1e4}}$_.nextNonX=$a();$_.isD=function(){$k[$j++]=$_.char>=48&&$_.char<=57};$_.isC=function(){$k[$j++]=$has($_.CNvals,$_.char)};$_.isT=function(){$k[$j++]=$has($_.TNvals,$_.char)};$_.isX=function(){$k[$j++]=$has($_.Xvals,$_.char)};$_.isE=function(){$k[$j++]=$has($_.Evals,$_.char)};$_.isEA=function(){$k[$j++]=$_.char>127};$_.isFN=function(){$k[$j++]=$_.char<0};$_.XtermFirst=function(){var _9g=$k[--$j];$k[$j++]=$lt($g($_.nextXterm,_9g),$g($_.nextNonX,_9g))};$_.lookup=function(){$_.ac=1;$_.cc=2;$_.tc=2;$_.xc=2;$_.ec=2;$_.bc=2.25;if($_.mode==$_.datamatrix_A){$_.ac=0;$_.cc=1;$_.tc=1;$_.xc=1;$_.ec=1;$_.bc=1.25}if($_.mode==$_.datamatrix_C){$_.cc=0}if($_.mode==$_.datamatrix_T){$_.tc=0}if($_.mode==$_.datamatrix_X){$_.xc=0}if($_.mode==$_.datamatrix_E){$_.ec=0}if($_.mode==$_.datamatrix_B){$_.bc=0}for(var _9r=0;_9r<1;_9r++){if($g($_.isECI,$_.i)){$k[$j++]=$_.datamatrix_A;break}if($_.i<$_.c40headerlength){$k[$j++]=$_.datamatrix_C;break}$_.isfnc1=$g($_.msg,$_.i)==$_.datamatrix_fnc1;$_.k=0;for(;;){if($_.i+$_.k==$_.msglen){var _A3=$a(["ac","cc","tc","xc","ec","bc"]);for(var _A4=0,_A5=_A3.length;_A4<_A5;_A4++){var _A6=$g(_A3,_A4);var _A7=$_[_A6];$_[_A6]=$ceil(_A7)}var _AD=$a([$_.cc,$_.tc,$_.xc,$_.ec,$_.bc]);var _AJ=true;for(var _AE=0,_AF=_AD.length;_AE<_AF;_AE++){_AJ=_AJ&&$_.ac<=$g(_AD,_AE)}if(_AJ){$k[$j++]=$_.datamatrix_A;break}var _AP=$a([$_.ac,$_.cc,$_.tc,$_.xc,$_.ec]);var _AV=true;for(var _AQ=0,_AR=_AP.length;_AQ<_AR;_AQ++){_AV=_AV&&$_.bc<$g(_AP,_AQ)}if(_AV){if($_.isfnc1){$k[$j++]=$_.datamatrix_A}else{$k[$j++]=$_.datamatrix_B}break}var _Ac=$a([$_.ac,$_.cc,$_.tc,$_.xc,$_.bc]);var _Ai=true;for(var _Ad=0,_Ae=_Ac.length;_Ad<_Ae;_Ad++){_Ai=_Ai&&$_.ec<$g(_Ac,_Ad)}if(_Ai){$k[$j++]=$_.datamatrix_E;break}var _Ao=$a([$_.ac,$_.cc,$_.xc,$_.ec,$_.bc]);var _Au=true;for(var _Ap=0,_Aq=_Ao.length;_Ap<_Aq;_Ap++){_Au=_Au&&$_.tc<$g(_Ao,_Ap)}if(_Au){$k[$j++]=$_.datamatrix_T;break}var _B0=$a([$_.ac,$_.cc,$_.tc,$_.ec,$_.bc]);var _B6=true;for(var _B1=0,_B2=_B0.length;_B1<_B2;_B1++){_B6=_B6&&$_.xc<$g(_B0,_B1)}if(_B6){$k[$j++]=$_.datamatrix_X;break}$k[$j++]=$_.datamatrix_C;break}$_.char=$g($_.msg,$_.i+$_.k);$k[$j++]="ac";$k[$j++]=$_.ac;$_.isD();var _BC=$k[--$j];if(_BC){var _BD=$k[--$j];$k[$j++]=$f(_BD+1/2)}else{$_.isEA();var _BE=$k[--$j];if(_BE){var _BF=$k[--$j];$k[$j++]=$ceil(_BF)+2}else{var _BG=$k[--$j];$k[$j++]=$ceil(_BG)+1}}var _BH=$k[--$j];var _BI=$k[--$j];$_[_BI]=_BH;$k[$j++]="cc";$k[$j++]=$_.cc;$_.isC();var _BK=$k[--$j];if(_BK){var _BL=$k[--$j];$k[$j++]=$f(_BL+.6666667)}else{$_.isEA();var _BM=$k[--$j];if(_BM){var _BN=$k[--$j];$k[$j++]=$f(_BN+2.6666667)}else{var _BO=$k[--$j];$k[$j++]=$f(_BO+1.3333334)}}var _BP=$k[--$j];var _BQ=$k[--$j];$_[_BQ]=_BP;$k[$j++]="tc";$k[$j++]=$_.tc;$_.isT();var _BS=$k[--$j];if(_BS){var _BT=$k[--$j];$k[$j++]=$f(_BT+.6666667)}else{$_.isEA();var _BU=$k[--$j];if(_BU){var _BV=$k[--$j];$k[$j++]=$f(_BV+2.6666667)}else{var _BW=$k[--$j];$k[$j++]=$f(_BW+1.3333334)}}var _BX=$k[--$j];var _BY=$k[--$j];$_[_BY]=_BX;$k[$j++]="xc";$k[$j++]=$_.xc;$_.isX();var _Ba=$k[--$j];if(_Ba){var _Bb=$k[--$j];$k[$j++]=$f(_Bb+.6666667)}else{$_.isEA();var _Bc=$k[--$j];if(_Bc){var _Bd=$k[--$j];$k[$j++]=$f(_Bd+4.3333334)}else{var _Be=$k[--$j];$k[$j++]=$f(_Be+3.3333334)}}var _Bf=$k[--$j];var _Bg=$k[--$j];$_[_Bg]=_Bf;$k[$j++]="ec";$k[$j++]=$_.ec;$_.isE();var _Bi=$k[--$j];if(_Bi){var _Bj=$k[--$j];$k[$j++]=$f(_Bj+3/4)}else{$_.isEA();var _Bk=$k[--$j];if(_Bk){var _Bl=$k[--$j];$k[$j++]=$f(_Bl+17/4)}else{var _Bm=$k[--$j];$k[$j++]=$f(_Bm+13/4)}}var _Bn=$k[--$j];var _Bo=$k[--$j];$_[_Bo]=_Bn;$k[$j++]="bc";$k[$j++]=$_.bc;$_.isFN();var _Bq=$k[--$j];if(_Bq){$k[$j-1]+=4}else{$k[$j-1]+=1}var _Bt=$k[--$j];var _Bu=$k[--$j];$_[_Bu]=_Bt;if($_.k>=4){var _C1=$a([$_.cc,$_.tc,$_.xc,$_.ec,$_.bc]);var _C7=true;for(var _C2=0,_C3=_C1.length;_C2<_C3;_C2++){_C7=_C7&&$_.ac+1<=$g(_C1,_C2)}if(_C7){$k[$j++]=$_.datamatrix_A;break}if($_.bc+1<=$_.ac){if($_.isfnc1){$k[$j++]=$_.datamatrix_A}else{$k[$j++]=$_.datamatrix_B}break}var _CF=$a([$_.cc,$_.tc,$_.xc,$_.ec]);var _CL=true;for(var _CG=0,_CH=_CF.length;_CG<_CH;_CG++){_CL=_CL&&$_.bc+1<$g(_CF,_CG)}if(_CL){if($_.isfnc1){$k[$j++]=$_.datamatrix_A}else{$k[$j++]=$_.datamatrix_B}break}var _CS=$a([$_.ac,$_.cc,$_.tc,$_.xc,$_.bc]);var _CY=true;for(var _CT=0,_CU=_CS.length;_CT<_CU;_CT++){_CY=_CY&&$_.ec+1<$g(_CS,_CT)}if(_CY){$k[$j++]=$_.datamatrix_E;break}var _Ce=$a([$_.ac,$_.cc,$_.xc,$_.ec,$_.bc]);var _Ck=true;for(var _Cf=0,_Cg=_Ce.length;_Cf<_Cg;_Cf++){_Ck=_Ck&&$_.tc+1<$g(_Ce,_Cf)}if(_Ck){$k[$j++]=$_.datamatrix_T;break}var _Cq=$a([$_.ac,$_.cc,$_.tc,$_.ec,$_.bc]);var _Cw=true;for(var _Cr=0,_Cs=_Cq.length;_Cr<_Cs;_Cr++){_Cw=_Cw&&$_.xc+1<$g(_Cq,_Cr)}if(_Cw){$k[$j++]=$_.datamatrix_X;break}var _D1=$a([$_.ac,$_.tc,$_.ec,$_.bc]);var _D7=true;for(var _D2=0,_D3=_D1.length;_D2<_D3;_D2++){_D7=_D7&&$_.cc+1<$g(_D1,_D2)}if(_D7){if($_.cc<$_.xc){$k[$j++]=$_.datamatrix_C;break}if($_.cc==$_.xc){$k[$j++]=$_.i+$_.k+1;$_.XtermFirst();var _DE=$k[--$j];if(_DE){$k[$j++]=$_.datamatrix_X;break}else{$k[$j++]=$_.datamatrix_C;break}}}}$_.k=$_.k+1}}};$_.addtocws=function(){var _DG=$k[$j-1];if(_DG.length+$_.j>$_.cws.length){$k[$j-1]="bwipp.datamatrixTooMuchData#25405";$k[$j++]="The input data exceeds the symbol capacity";bwipp_raiseerror()}var _DJ=$k[--$j];$P($_.cws,$_.j,_DJ);$_.j=_DJ.length+$_.j};$_.ECItocws=function(){var _DN=$k[--$j];var _DO=-_DN-1e6;$k[$j++]=_DO;if(_DO<=126){var _DP=$k[--$j];var _DQ=$a(1);$k[$j++]=_DP+1;$as(_DQ)}else{var _DR=$k[$j-1];if(_DR<=16382){var _DS=$k[--$j];var _DT=_DS-127;var _DU=$a(2);$k[$j++]=~~(_DT/254)+128;$k[$j++]=_DT%254+1;$as(_DU)}else{var _DV=$k[--$j];var _DW=_DV-16383;var _DX=$a(3);$k[$j++]=~~(_DW/64516)+192;$k[$j++]=~~(_DW/254)%254+1;$k[$j++]=_DW%254+1;$as(_DX)}}};$_.encA=function(){for(var _DY=0;_DY<1;_DY++){if($g($_.isECI,$_.i)){$k[$j++]=$g($_.Avals,$_.datamatrix_eci);$_.addtocws();$k[$j++]=$g($_.msg,$_.i);$_.ECItocws();$_.addtocws();$_.i=$_.i+1;break}if($_.i>=$_.c40headerlength&&$g($_.numD,$_.i)>=2){var _Dn=$s(2);$p(_Dn,0,$g($_.msg,$_.i));$p(_Dn,1,$g($_.msg,$_.i+1));$k[$j++]=$g($_.Avals,_Dn);$_.addtocws();$_.i=$_.i+2;break}$k[$j++]="newmode";$_.lookup();var _Dx=$k[--$j];var _Dy=$k[--$j];$_[_Dy]=_Dx;if($_.newmode!=$_.mode){$k[$j++]=$g($_.Avals,$g($a([-1,$_.datamatrix_lC,$_.datamatrix_lT,$_.datamatrix_lX,$_.datamatrix_lE,$_.datamatrix_lB]),$_.newmode));$_.addtocws();$_.mode=$_.newmode;break}$k[$j++]=$g($_.Avals,$g($_.msg,$_.i));$_.addtocws();$_.i=$_.i+1;break}};$_.CTXvalstocws=function(){var _ED=$k[--$j];$_.in=_ED;$k[$j++]=Infinity;for(var _EG=0,_EF=$_.in.length-1;_EG<=_EF;_EG+=3){var _EI=$G($_.in,_EG,3);var _EN=0;for(var _EJ=0,_EK=_EI.length;_EJ<_EK;_EJ++){_EN=$f(_EN+$g(_EI,_EJ))*40}var _EO=~~(_EN/40)+1;$k[$j++]=~~(_EO/256);$k[$j++]=_EO%256}$as($a($m()));var _ER=$k[--$j];$k[$j-1]=_ER};$_.encCTX=function(){$_.p=0;$_.ctxvals=$a(2500);$_.done=false;for(;;){if($_.i==$_.msglen){break}if(!$has($g($_.encvals,$_.mode),$g($_.msg,$_.i))){break}if($_.p%3==0){if($_.p>0){$k[$j++]="newmode";$_.lookup();var _Ef=$k[--$j];var _Eg=$k[--$j];$_[_Eg]=_Ef;if($ne($_.newmode,$_.mode)){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.datamatrix_unlcw]);$_.addtocws();if($_.newmode!=$_.datamatrix_A){$k[$j++]=$g($_.Avals,$g($a([-1,$_.datamatrix_lC,$_.datamatrix_lT,$_.datamatrix_lX,$_.datamatrix_lE,$_.datamatrix_lB]),$_.newmode));$_.addtocws()}$_.mode=$_.newmode;$_.done=true;break}}if($_.msglen-$_.i<=3){$k[$j++]=$_.j+~~($_.p/3)*2;$_.getnumremcws();var _Ey=$k[--$j];$_.remcws=_Ey;$k[$j++]=Infinity;var _F3=$G($_.msg,$_.i,$_.msglen-$_.i);for(var _F4=0,_F5=_F3.length;_F4<_F5;_F4++){var _F6=$g(_F3,_F4);var _FA=$has($g($_.encvals,$_.mode),_F6);$k[$j++]=_F6;if(_FA){var _FE=$k[--$j];$q($g($g($_.encvals,$_.mode),_FE))}else{$k[$j-1]=-1;$k[$j++]=-1;$k[$j++]=-1;$k[$j++]=-1}}$_.remvals=$a();if($_.remcws==2&&$_.remvals.length==3){$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));$q($_.remvals);var _FM=$a();$k[$j++]=_FM;$_.CTXvalstocws();$_.addtocws();$_.mode=$_.datamatrix_A;$_.i=$_.msglen;$_.done=true;break}if($_.remcws==2&&$_.remvals.length==2&&$_.mode!=$_.datamatrix_X){$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));$q($_.remvals);$q($g($g($_.encvals,$_.mode),$_.datamatrix_sft1));var _FZ=$a();$k[$j++]=_FZ;$_.CTXvalstocws();$_.addtocws();$_.mode=$_.datamatrix_A;$_.i=$_.msglen;$_.done=true;break}if($_.remcws==2&&$_.remvals.length==1){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.datamatrix_unlcw]);$_.addtocws();$k[$j++]=$g($_.Avals,$g($_.msg,$_.i));$_.addtocws();$_.mode=$_.datamatrix_A;$_.i=$_.msglen;$_.done=true;break}if($_.remcws==1&&$_.remvals.length==1){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$g($_.Avals,$g($_.msg,$_.i));$_.addtocws();$_.mode=$_.datamatrix_A;$_.i=$_.msglen;$_.done=true;break}}}var _G4=$g($g($_.encvals,$_.mode),$g($_.msg,$_.i));$k[$j++]=_G4;if(_G4.length+$_.p>$_.ctxvals.length){$k[$j-1]="bwipp.datamatrixTooMuchData#25545";$k[$j++]="The input data exceeds the symbol capacity";bwipp_raiseerror()}var _G7=$k[--$j];$P($_.ctxvals,$_.p,_G7);$_.p=_G7.length+$_.p;$_.i=$_.i+1}if(!$_.done){for(;;){if($_.p%3==0){break}$_.i=$_.i-1;$_.p=$_.p-$g($g($_.encvals,$_.mode),$g($_.msg,$_.i)).length}$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));var _GQ=$a();$k[$j++]=_GQ;$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.datamatrix_unlcw]);$_.addtocws();$_.mode=$_.datamatrix_A;$_.c40headerlength=-1;if($_.i!=$_.msglen&&!$g($_.isECI,$_.i)){if($g($_.numD,$_.i)>=2){var _Ga=$s(2);$p(_Ga,0,$g($_.msg,$_.i));$p(_Ga,1,$g($_.msg,$_.i+1));$k[$j++]=$g($_.Avals,_Ga);$_.addtocws();$_.i=$_.i+2}else{$k[$j++]=$g($_.Avals,$g($_.msg,$_.i));$_.addtocws();$_.i=$_.i+1}}}};$_.Evalstocws=function(){var _Gq=$k[--$j];$_.in=_Gq;$_.inlen=$_.in.length;$_.outlen=$ceil($_.in.length/4*3);$k[$j++]=Infinity;$q($_.in);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$_.in=$a();$k[$j++]=Infinity;for(var _Gw=0,_Gv=$_.inlen-1;_Gw<=_Gv;_Gw+=4){var _Gy=$G($_.in,_Gw,4);var _H3=0;for(var _Gz=0,_H0=_Gy.length;_Gz<_H0;_Gz++){_H3=$or(_H3,$g(_Gy,_Gz))*64}var _H4=$flr(_H3/64);$k[$j++]=$flr(_H4/65536)&255;$k[$j++]=$flr(_H4/256)&255;$k[$j++]=_H4&255}$as($a($m()));var _H7=$k[--$j];$k[$j-1]=$G(_H7,0,$_.outlen)};$_.encE=function(){$_.p=0;$_.edifactvals=$a(2100);for(;;){if($_.i==$_.msglen){break}if(!$has($_.Evals,$g($_.msg,$_.i))){break}if($_.p>=$_.edifactvals.length){$k[$j++]="bwipp.datamatrixTooMuchData#25608";$k[$j++]="The input data exceeds the symbol capacity";bwipp_raiseerror()}if($_.p%4==0){if($_.msglen-$_.i<=2){$k[$j++]=$_.j+~~($_.p/4)*3;$_.getnumremcws();var _HR=$k[--$j];$_.remcws=_HR;$k[$j++]=Infinity;var _HW=$G($_.msg,$_.i,$_.msglen-$_.i);for(var _HX=0,_HY=_HW.length;_HX<_HY;_HX++){$q($g($_.Avals,$g(_HW,_HX)))}$_.remvals=$a();if(($_.remcws==1||$_.remcws==2)&&$_.remvals.length<=$_.remcws){$k[$j++]=$G($_.edifactvals,0,$_.p);$_.Evalstocws();$_.addtocws();$k[$j++]=$_.remvals;$_.addtocws();$_.mode=$_.datamatrix_A;$_.i=$_.msglen;break}}$_.lookup();var _Hm=$k[--$j];if(_Hm!=$_.mode){break}}var _Hr=$g($_.Evals,$g($_.msg,$_.i));$P($_.edifactvals,$_.p,_Hr);$_.p=_Hr.length+$_.p;$_.i=$_.i+1}if($_.mode!=$_.datamatrix_A){$k[$j++]=$_.j+~~($_.p/4)*3-1;$_.getnumremcws();var _Hz=$k[--$j];$_.remcws=_Hz-1;if($_.p%4!=0||$_.i!=$_.msglen||$_.remcws>=3){var _I5=$g($_.Evals,$_.datamatrix_unl);$P($_.edifactvals,$_.p,_I5);$_.p=_I5.length+$_.p}$k[$j++]=$G($_.edifactvals,0,$_.p);$_.Evalstocws();$_.addtocws();$_.mode=$_.datamatrix_A;$_.c40headerlength=-1;if($_.i!=$_.msglen&&!$g($_.isECI,$_.i)){if($g($_.numD,$_.i)>=2){var _IK=$s(2);$p(_IK,0,$g($_.msg,$_.i));$p(_IK,1,$g($_.msg,$_.i+1));$k[$j++]=$g($_.Avals,_IK);$_.addtocws();$_.i=$_.i+2}else{$k[$j++]=$g($_.Avals,$g($_.msg,$_.i));$_.addtocws();$_.i=$_.i+1}}}};$_.encB=function(){$_.p=0;$_.bvals=$a(1558);for(;;){if($_.i==$_.msglen){break}$_.lookup();var _Ie=$k[--$j];if(_Ie!=$_.mode){break}if($_.p>=$_.bvals.length){$k[$j++]="bwipp.datamatrixTooMuchData#25667";$k[$j++]="The input data exceeds the symbol capacity";bwipp_raiseerror()}$p($_.bvals,$_.p,$g($_.msg,$_.i));$_.p=$_.p+1;$_.i=$_.i+1}$k[$j++]=$_.j+$_.p;$_.getnumremcws();var _Iq=$k[--$j];$_.remcws=_Iq-1;$k[$j++]=Infinity;if($_.remcws==0&&$_.i==$_.msglen){$k[$j++]=0}else{if($_.p<250){$k[$j++]=$_.p}else{$k[$j++]=~~($_.p/250)+249;$k[$j++]=$_.p%250}}$q($G($_.bvals,0,$_.p));$_.bvals=$a();for(var _J3=0,_J2=$_.bvals.length-1;_J3<=_J2;_J3+=1){$_.p=_J3;var _J9=$f(($_.j+$_.p+1)*149%255+1+$g($_.bvals,$_.p));if(_J9>=256){_J9=_J9-256}$p($_.bvals,$_.p,_J9)}$k[$j++]=$_.bvals;$_.addtocws();$_.mode=$_.datamatrix_A};$_.cws=$a(1558);$_.mode=$_.datamatrix_A;$_.i=0;$_.j=0;if($g($_.msg,0)==$_.datamatrix_fnc1){$k[$j++]=$g($_.Avals,$_.datamatrix_fnc1);$_.addtocws();$_.i=1;if($_.c40headerlength>0){$_.c40headerlength=$_.c40headerlength+1}}for(;;){if($_.i>=$_.msglen){break}var _JQ=$_[$g($_.datamatrix_encfuncs,$_.mode)];if(_JQ()===true){break}}$_.cws=$G($_.cws,0,$_.j)}$_.datlen=$_.cws.length;$k[$j++]=$_.j-1;$_.getnumremcws();var _JW=$k[--$j];$_.remcws=_JW-1;if($_.remcws>0){$k[$j++]=Infinity;$q($_.cws);for(var _Ja=0,_Jb=$_.remcws;_Ja<_Jb;_Ja++){$k[$j++]=129}$_.cws=$a();for(var _Jg=$_.datlen+1,_Jf=$f($_.datlen+$_.remcws)-1;_Jg<=_Jf;_Jg+=1){$_.i=_Jg;var _Ji=($_.i+1)*149%253+1+129;if(_Ji>254){_Ji=_Ji-254}$p($_.cws,$_.i,_Ji)}}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#25729";$k[$j++]=$_.cws;bwipp_raiseerror()}for(var _Jq=0,_Jr=$_.datamatrix_metrics.length;_Jq<_Jr;_Jq++){$_.m=$g($_.datamatrix_metrics,_Jq);$_.rows=$g($_.m,0);$_.cols=$g($_.m,1);$_.regh=$g($_.m,2);$_.regv=$g($_.m,3);$_.rscw=$g($_.m,4);$_.rsbl=$g($_.m,5);$_.doly=$g($_.m,6);$_.mrows=$f($_.rows-2*$_.regh);$_.mcols=$f($_.cols-2*$_.regv);$_.rrows=~~($_.mrows/$_.regh);$_.rcols=~~($_.mcols/$_.regv);$_.ncws=$f(~~($_.mrows*$_.mcols/8)-$_.rscw);$_.okay=true;if($_.cws.length!=$_.ncws){$_.okay=false}if($_.urows!=0&&$_.urows!=$_.rows){$_.okay=false}if($_.ucols!=0&&$_.ucols!=$_.cols){$_.okay=false}if($eq($_.format,"square")&&$ne($_.rows,$_.cols)){$_.okay=false}if($eq($_.format,"rectangle")&&$eq($_.rows,$_.cols)){$_.okay=false}if(!$_.dmre&&$_.doly==1){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.datamatrixNoValidSymbol#25761";$k[$j++]="Maximum length exceeded or invalid size";bwipp_raiseerror()}$k[$j++]="coeffs";$k[$j++]=~~($_.rscw/$_.rsbl);$_.datamatrix_coeffscachefetch();var _Kc=$k[--$j];var _Kd=$k[--$j];$_[_Kd]=_Kc;$_.ecbs=$a($_.rsbl);$_.ecpb=~~($_.rscw/$_.rsbl);for(var _Kk=0,_Kj=$_.rsbl-1;_Kk<=_Kj;_Kk+=1){$_.blk=_Kk;if($_.cws.length!=1558){$_.cwbsize=~~($_.cws.length/$_.rsbl)}else{if($_.blk<=7){$_.cwbsize=156}else{$_.cwbsize=155}}$_.lfsr=$a($_.ecpb);var _Kt=$_.cwbsize;var _Ku=$_.coeffs;var _Kv=$_.ecpb;var _Kw=$_.lfsr;$k[$j++]=$_.ecbs;$k[$j++]=$_.blk;$k[$j++]=function(){var _Ky=$k[--$j];$k[$j++]=$g($_.cws,$f(_Ky*$_.rsbl+$_.blk))};$k[$j++]=_Kt;$k[$j++]=_Ku;$k[$j++]=_Kv;$k[$j++]=_Kw;$k[$j++]=$_.datamatrix_rsprod;bwipp_rsecbinary();var _L3=$k[--$j];var _L4=$k[--$j];$p(_L4,_L3,$_.lfsr)}if($_.ncws==1558){$k[$j++]=Infinity;$q($G($_.ecbs,8,2));$q($G($_.ecbs,0,8));$_.ecbs=$a()}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#25805";$k[$j++]=Infinity;var _LC=$_.ecbs;for(var _LD=0,_LE=_LC.length;_LD<_LE;_LD++){$q($g(_LC,_LD))}var _LG=$a();$k[$j++]=_LG;bwipp_raiseerror()}var _LJ=$a($f($_.cws.length+$_.rscw));$P(_LJ,0,$_.cws);$_.cws=_LJ;for(var _LN=0,_LM=$_.rscw-1;_LN<=_LM;_LN+=1){$_.i=_LN;$p($_.cws,$f($_.ncws+$_.i),$g($g($_.ecbs,$_.i%$_.rsbl),~~($_.i/$_.rsbl)))}$_.module=function(){$r(4,-1);var _LZ=$Z($s(8),"00000000");$k[$j++]=_LZ;$k[$j++]=_LZ;var _La=$s(8);var _Lb=$k[$j-3];var _Lc=$R(_La,_Lb,2);var _Ld=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;$P(_Ld,8-_Lc.length,_Lc);for(var _Le=7;_Le>=0;_Le-=1){var _Lf=$k[--$j];$k[$j++]=$g(_Lf,_Le)-48;$k[$j++]=_Lf}$j--;$r(11,-3);var _Lh=$k[--$j];$F(_Lh,function(){var _Li=$k[--$j];if(_Li()===true){return true}var _Lk=$k[$j-2];if(_Lk<0){var _Ll=$k[--$j];var _Lm=$k[--$j];$k[$j++]=$f(_Lm+$_.mrows);$k[$j++]=$f(_Ll+(4-($_.mrows+4)%8))}var _Lp=$k[$j-1];if(_Lp<0){var _Lr=$k[--$j];var _Ls=$k[--$j];$k[$j++]=$f(_Ls+(4-($_.mcols+4)%8));$k[$j++]=$f(_Lr+$_.mcols)}var _Lv=$k[$j-2];if(_Lv>=$_.mrows){var _Lx=$k[--$j];var _Ly=$k[--$j];$k[$j++]=$f(_Ly-$_.mrows);$k[$j++]=_Lx}var _M0=$k[--$j];var _M1=$k[--$j];$k[$j++]=$_.mmat;$k[$j++]=$f(_M0+_M1*$_.mcols);$r(5,-1);var _M4=$k[--$j];var _M5=$k[--$j];var _M6=$k[--$j];$p(_M6,_M5,_M4)})};$k[$j++]=Infinity;for(var _M9=0,_MA=$_.mrows*$_.mcols;_M9<_MA;_M9++){$k[$j++]=-1}$_.mmat=$a();for(var _MC=$_.cws.length-1;_MC>=0;_MC-=1){$k[$j++]=$g($_.cws,_MC)}$k[$j++]=4;$k[$j++]=0;for(;;){var _MF=$k[$j-1];var _MG=$k[$j-2];if(_MF==0&&_MG==$_.mrows){$k[$j++]=$_.datamatrix_dmc1;$_.module()}var _MI=$k[$j-1];var _MJ=$k[$j-2];if(_MI==0&&_MJ==$_.mrows-2&&$_.mcols%4!=0){$k[$j++]=$_.datamatrix_dmc2;$_.module()}var _MM=$k[$j-1];var _MN=$k[$j-2];if(_MM==0&&_MN==$_.mrows-2&&$_.mcols%8==4){$k[$j++]=$_.datamatrix_dmc3;$_.module()}var _MQ=$k[$j-1];var _MR=$k[$j-2];if(_MQ==2&&_MR==$_.mrows+4&&$_.mcols%8==0){$k[$j++]=$_.datamatrix_dmc4;$_.module()}for(;;){var _MU=$k[$j-1];var _MV=$k[$j-2];if(_MU>=0&&_MV<$_.mrows){var _MX=$k[$j-1];var _MY=$k[$j-2];var _Mb=$g($_.mmat,$f(_MX+_MY*$_.mcols));if(_Mb==-1){$k[$j++]=$_.datamatrix_dmn;$_.module()}}var _Mc=$k[--$j];var _Md=$k[--$j];$k[$j++]=_Md-2;$k[$j++]=_Mc+2;if(!(_Mc+2<$_.mcols&&_Md-2>=0)){break}}var _Mf=$k[--$j];$k[$j-1]+=1;$k[$j++]=_Mf+3;for(;;){var _Mh=$k[$j-1];var _Mi=$k[$j-2];if(_Mh<$_.mcols&&_Mi>=0){var _Mk=$k[$j-1];var _Ml=$k[$j-2];var _Mo=$g($_.mmat,$f(_Mk+_Ml*$_.mcols));if(_Mo==-1){$k[$j++]=$_.datamatrix_dmn;$_.module()}}var _Mp=$k[--$j];var _Mq=$k[--$j];$k[$j++]=_Mq+2;$k[$j++]=_Mp-2;if(!(_Mp-2>=0&&_Mq+2<$_.mrows)){break}}var _Ms=$k[--$j];var _Mt=$k[--$j];$k[$j++]=_Mt+3;$k[$j++]=_Ms+1;if(!(_Ms+1<$_.mcols||_Mt+3<$_.mrows)){$j-=2;break}}if($g($_.mmat,$_.mrows*$_.mcols-1)==-1){$P($_.mmat,$_.mrows*($_.mcols-1)-2,$a([1,0]));$P($_.mmat,$_.mrows*$_.mcols-2,$a([0,1]))}$_.pixs=$a($_.rows*$_.cols);$_.cwpos=0;for(var _ND=0,_NC=$_.rows-1;_ND<=_NC;_ND+=1){$_.i=_ND;if($_.i%($_.rrows+2)==0){var _NO=$_.pixs;var _NN=$_.i*$_.cols;$k[$j++]=Infinity;for(var _NK=0,_NL=~~($_.cols/2);_NK<_NL;_NK++){$k[$j++]=1;$k[$j++]=0}var _NM=$a();$P(_NO,_NN,_NM)}if($_.i%($_.rrows+2)==$_.rrows+1){var _Na=$_.pixs;var _NZ=$_.i*$_.cols;var _NY=[];for(var _NW=0,_NX=$_.cols;_NW<_NX;_NW++){_NY[_NW]=1}_NY=$a(_NY);$P(_Na,_NZ,_NY)}if($_.i%($_.rrows+2)!=0&&$_.i%($_.rrows+2)!=$_.rrows+1){for(var _Ni=0,_Nh=$_.cols-1;_Ni<=_Nh;_Ni+=1){$_.j=_Ni;if($_.j%($_.rcols+2)==0){$p($_.pixs,$f($_.i*$_.cols+$_.j),1)}if($_.j%($_.rcols+2)==$_.rcols+1){$p($_.pixs,$f($_.i*$_.cols+$_.j),$_.i%2)}if($_.j%($_.rcols+2)!=0&&$_.j%($_.rcols+2)!=$_.rcols+1){$p($_.pixs,$f($_.i*$_.cols+$_.j),$g($_.mmat,$_.cwpos));$_.cwpos=$_.cwpos+1}}}}$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.cols],["pixy",$_.rows],["height",$_.rows*$_.modunit/72],["width",$_.cols*$_.modunit/72],["borderleft",1],["borderright",1],["bordertop",1],["borderbottom",1],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_datamatrixrectangular(){var $__=$_;$_=Object.assign({},$_,bwipp_datamatrixrectangular.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","rectangle");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_datamatrixrectangularextension(){var $__=$_;$_=Object.assign({},$_,bwipp_datamatrixrectangularextension.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"dmre",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_mailmark(){if(!bwipp_mailmark.globals){var $__=$_;$_=bwipp_mailmark.globals={};var _0=new Map([["7","24x24"],["9","32x32"],["29","16x48"]]);$_.mailmark_typetoversion=_0;var _1=new Map([["7","square"],["9","square"],["29","rectangle"]]);$_.mailmark_typetoformat=_1;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_mailmark.globals);$_.type="unset";$_.parse=false;$_.dontdraw=false;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _9=$k[--$j];$_.barcode=_9;$_.barlen=$_.barcode.length;$del($_.options,"parse");var _C=$_.type;if($ne(_C,"7")&&($ne(_C,"9")&&$ne(_C,"29"))){$k[$j++]="bwipp.mailmarkBadType#26146";$k[$j++]="Royal Mail Mailmark type must be 7, 9 or 29";bwipp_raiseerror()}$_.version=$g($_.mailmark_typetoversion,$_.type);$_.format=$g($_.mailmark_typetoformat,$_.type);if($_.barcode.length<45){$k[$j++]="bwipp.mailmarkBadLength#26155";$k[$j++]="Royal Mail Mailmark must contain at least 45 characters of Mailmark formatted data, including any required space padding";bwipp_raiseerror()}if($ne($G($_.barcode,0,4),"JGB ")){$k[$j++]="bwipp.mailmarkBadIndicator#26158";$k[$j++]="Royal Mail Mailmark must begin with JGB identifier";bwipp_raiseerror()}$p($_.options,"version",$_.version);$p($_.options,"format",$_.format);$p($_.options,"c40headerlength",45);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _P=$k[--$j];$_.args=_P;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_qrcode(){if(!bwipp_qrcode.globals){var $__=$_;$_=bwipp_qrcode.globals={};$_.qrcode_v1to9=0;$_.qrcode_v10to26=1;$_.qrcode_v27to40=2;$_.qrcode_vM1=3;$_.qrcode_vM2=4;$_.qrcode_vM3=5;$_.qrcode_vM4=6;$_.qrcode_vR7x43=7;$_.qrcode_vR7x59=8;$_.qrcode_vR7x77=9;$_.qrcode_vR7x99=10;$_.qrcode_vR7x139=11;$_.qrcode_vR9x43=12;$_.qrcode_vR9x59=13;$_.qrcode_vR9x77=14;$_.qrcode_vR9x99=15;$_.qrcode_vR9x139=16;$_.qrcode_vR11x27=17;$_.qrcode_vR11x43=18;$_.qrcode_vR11x59=19;$_.qrcode_vR11x77=20;$_.qrcode_vR11x99=21;$_.qrcode_vR11x139=22;$_.qrcode_vR13x27=23;$_.qrcode_vR13x43=24;$_.qrcode_vR13x59=25;$_.qrcode_vR13x77=26;$_.qrcode_vR13x99=27;$_.qrcode_vR13x139=28;$_.qrcode_vR15x43=29;$_.qrcode_vR15x59=30;$_.qrcode_vR15x77=31;$_.qrcode_vR15x99=32;$_.qrcode_vR15x139=33;$_.qrcode_vR17x43=34;$_.qrcode_vR17x59=35;$_.qrcode_vR17x77=36;$_.qrcode_vR17x99=37;$_.qrcode_vR17x139=38;$_.qrcode_N=0;$_.qrcode_A=1;$_.qrcode_B=2;$_.qrcode_K=3;$_.qrcode_E=4;$_.qrcode_encfuncs=$a(["encN","encA","encB","encK","encE"]);$_.qrcode_fn1=-1;$k[$j++]=Infinity;$k[$j++]=Infinity;for(var _1=48;_1<=57;_1+=1){$k[$j++]=_1}var _2=$a();for(var _3=0,_4=_2.length;_3<_4;_3++){$k[$j++]=$g(_2,_3);$k[$j++]=-1}var _6=$d();$_.qrcode_Nexcl=_6;$k[$j++]=Infinity;$k[$j++]=Infinity;$k[$j++]=32;$k[$j++]=36;$k[$j++]=37;$k[$j++]=42;$k[$j++]=43;$k[$j++]=45;$k[$j++]=46;$k[$j++]=47;$k[$j++]=58;for(var _7=65;_7<=90;_7+=1){$k[$j++]=_7}$k[$j++]=$_.qrcode_fn1;var _8=$a();for(var _9=0,_A=_8.length;_9<_A;_9++){$k[$j++]=$g(_8,_9);$k[$j++]=-1}var _C=$d();$_.qrcode_Aexcl=_C;$k[$j++]=Infinity;$k[$j++]=Infinity;for(var _D=129;_D<=159;_D+=1){$k[$j++]=_D}for(var _E=224;_E<=235;_E+=1){$k[$j++]=_E}var _F=$a();for(var _G=0,_H=_F.length;_G<_H;_G++){$k[$j++]=$g(_F,_G);$k[$j++]=-1}var _J=$d();$_.qrcode_Kexcl=_J;$k[$j++]=Infinity;var _K=$a(["0001","0010","0100","1000","0111"]);$k[$j++]=_K;$k[$j++]=_K;$k[$j++]=_K;$k[$j++]=$a(["",-1,-1,-1,-1]);$k[$j++]=$a(["0","1",-1,-1,-1]);$k[$j++]=$a(["00","01","10","11",-1]);$k[$j++]=$a(["000","001","010","011",-1]);$k[$j++]=$a(["001","010","011","100","111"]);for(var _Q=0;_Q<31;_Q++){var _R=$k[$j-1];$k[$j++]=_R}var _S=$a();$_.qrcode_mids=_S;$_.qrcode_cclens=$a([$a([10,9,8,8]),$a([12,11,16,10]),$a([14,13,16,12]),$a([3,-1,-1,-1]),$a([4,3,-1,-1]),$a([5,4,4,3]),$a([6,5,5,4]),$a([4,3,3,2]),$a([5,5,4,3]),$a([6,5,5,4]),$a([7,6,5,5]),$a([7,6,6,5]),$a([5,5,4,3]),$a([6,5,5,4]),$a([7,6,5,5]),$a([7,6,6,5]),$a([8,7,6,6]),$a([4,4,3,2]),$a([6,5,5,4]),$a([7,6,5,5]),$a([7,6,6,5]),$a([8,7,6,6]),$a([8,7,7,6]),$a([5,5,4,3]),$a([6,6,5,5]),$a([7,6,6,5]),$a([7,7,6,6]),$a([8,7,7,6]),$a([8,8,7,7]),$a([7,6,6,5]),$a([7,7,6,5]),$a([8,7,7,6]),$a([8,7,7,6]),$a([9,8,7,7]),$a([7,6,6,5]),$a([8,7,6,6]),$a([8,7,7,6]),$a([8,8,7,6]),$a([9,8,8,7])]);$k[$j++]=Infinity;for(var _17=0;_17<3;_17++){$k[$j++]=4}$k[$j++]=3;$k[$j++]=5;$k[$j++]=7;$k[$j++]=9;for(var _18=0;_18<32;_18++){$k[$j++]=3}var _19=$a();$_.qrcode_termlens=_19;$_.qrcode_padstrs=$a(["11101100","00010001"]);$_.qrcode_charmap="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";$k[$j++]=Infinity;$k[$j++]="full";$k[$j++]=Infinity;for(var _1B=0;_1B<=9;_1B+=1){$k[$j++]=$R($s(2),_1B,10);$k[$j++]=$_.qrcode_v1to9}for(var _1E=10;_1E<=26;_1E+=1){$k[$j++]=$R($s(2),_1E,10);$k[$j++]=$_.qrcode_v10to26}for(var _1H=27;_1H<=40;_1H+=1){$k[$j++]=$R($s(2),_1H,10);$k[$j++]=$_.qrcode_v27to40}var _1K=$d();var _1L=new Map([["M1",$_.qrcode_vM1],["M2",$_.qrcode_vM2],["M3",$_.qrcode_vM3],["M4",$_.qrcode_vM4]]);var _1M=new Map([["R7x43",$_.qrcode_vR7x43],["R7x59",$_.qrcode_vR7x59],["R7x77",$_.qrcode_vR7x77],["R7x99",$_.qrcode_vR7x99],["R7x139",$_.qrcode_vR7x139],["R9x43",$_.qrcode_vR9x43],["R9x59",$_.qrcode_vR9x59],["R9x77",$_.qrcode_vR9x77],["R9x99",$_.qrcode_vR9x99],["R9x139",$_.qrcode_vR9x139],["R11x27",$_.qrcode_vR11x27],["R11x43",$_.qrcode_vR11x43],["R11x59",$_.qrcode_vR11x59],["R11x77",$_.qrcode_vR11x77],["R11x99",$_.qrcode_vR11x99],["R11x139",$_.qrcode_vR11x139],["R13x27",$_.qrcode_vR13x27],["R13x43",$_.qrcode_vR13x43],["R13x59",$_.qrcode_vR13x59],["R13x77",$_.qrcode_vR13x77],["R13x99",$_.qrcode_vR13x99],["R13x139",$_.qrcode_vR13x139],["R15x43",$_.qrcode_vR15x43],["R15x59",$_.qrcode_vR15x59],["R15x77",$_.qrcode_vR15x77],["R15x99",$_.qrcode_vR15x99],["R15x139",$_.qrcode_vR15x139],["R17x43",$_.qrcode_vR17x43],["R17x59",$_.qrcode_vR17x59],["R17x77",$_.qrcode_vR17x77],["R17x99",$_.qrcode_vR17x99],["R17x139",$_.qrcode_vR17x139]]);$k[$j++]=_1K;$k[$j++]="micro";$k[$j++]=_1L;$k[$j++]="rmqr";$k[$j++]=_1M;var _1N=$d();$_.qrcode_versetmap=_1N;$_.qrcode_versetfull=$a([$_.qrcode_v1to9,$_.qrcode_v10to26,$_.qrcode_v27to40]);$_.qrcode_versetmicro=$a([$_.qrcode_vM1,$_.qrcode_vM2,$_.qrcode_vM3,$_.qrcode_vM4]);$_.qrcode_metrics=$a([$a(["micro","M1",$_.qrcode_vM1,11,11,98,99,36,$a([2,99,99,99]),$a([1,0,-1,-1,-1,-1,-1,-1])]),$a(["micro","M2",$_.qrcode_vM2,13,13,98,99,80,$a([5,6,99,99]),$a([1,0,1,0,-1,-1,-1,-1])]),$a(["micro","M3",$_.qrcode_vM3,15,15,98,99,132,$a([6,8,99,99]),$a([1,0,1,0,-1,-1,-1,-1])]),$a(["micro","M4",$_.qrcode_vM4,17,17,98,99,192,$a([8,10,14,99]),$a([1,0,1,0,1,0,-1,-1])]),$a(["full","1",$_.qrcode_v1to9,21,21,98,99,208,$a([7,10,13,17]),$a([1,0,1,0,1,0,1,0])]),$a(["full","2",$_.qrcode_v1to9,25,25,18,99,359,$a([10,16,22,28]),$a([1,0,1,0,1,0,1,0])]),$a(["full","3",$_.qrcode_v1to9,29,29,22,99,567,$a([15,26,36,44]),$a([1,0,1,0,2,0,2,0])]),$a(["full","4",$_.qrcode_v1to9,33,33,26,99,807,$a([20,36,52,64]),$a([1,0,2,0,2,0,4,0])]),$a(["full","5",$_.qrcode_v1to9,37,37,30,99,1079,$a([26,48,72,88]),$a([1,0,2,0,2,2,2,2])]),$a(["full","6",$_.qrcode_v1to9,41,41,34,99,1383,$a([36,64,96,112]),$a([2,0,4,0,4,0,4,0])]),$a(["full","7",$_.qrcode_v1to9,45,45,22,38,1568,$a([40,72,108,130]),$a([2,0,4,0,2,4,4,1])]),$a(["full","8",$_.qrcode_v1to9,49,49,24,42,1936,$a([48,88,132,156]),$a([2,0,2,2,4,2,4,2])]),$a(["full","9",$_.qrcode_v1to9,53,53,26,46,2336,$a([60,110,160,192]),$a([2,0,3,2,4,4,4,4])]),$a(["full","10",$_.qrcode_v10to26,57,57,28,50,2768,$a([72,130,192,224]),$a([2,2,4,1,6,2,6,2])]),$a(["full","11",$_.qrcode_v10to26,61,61,30,54,3232,$a([80,150,224,264]),$a([4,0,1,4,4,4,3,8])]),$a(["full","12",$_.qrcode_v10to26,65,65,32,58,3728,$a([96,176,260,308]),$a([2,2,6,2,4,6,7,4])]),$a(["full","13",$_.qrcode_v10to26,69,69,34,62,4256,$a([104,198,288,352]),$a([4,0,8,1,8,4,12,4])]),$a(["full","14",$_.qrcode_v10to26,73,73,26,46,4651,$a([120,216,320,384]),$a([3,1,4,5,11,5,11,5])]),$a(["full","15",$_.qrcode_v10to26,77,77,26,48,5243,$a([132,240,360,432]),$a([5,1,5,5,5,7,11,7])]),$a(["full","16",$_.qrcode_v10to26,81,81,26,50,5867,$a([144,280,408,480]),$a([5,1,7,3,15,2,3,13])]),$a(["full","17",$_.qrcode_v10to26,85,85,30,54,6523,$a([168,308,448,532]),$a([1,5,10,1,1,15,2,17])]),$a(["full","18",$_.qrcode_v10to26,89,89,30,56,7211,$a([180,338,504,588]),$a([5,1,9,4,17,1,2,19])]),$a(["full","19",$_.qrcode_v10to26,93,93,30,58,7931,$a([196,364,546,650]),$a([3,4,3,11,17,4,9,16])]),$a(["full","20",$_.qrcode_v10to26,97,97,34,62,8683,$a([224,416,600,700]),$a([3,5,3,13,15,5,15,10])]),$a(["full","21",$_.qrcode_v10to26,101,101,28,50,9252,$a([224,442,644,750]),$a([4,4,17,0,17,6,19,6])]),$a(["full","22",$_.qrcode_v10to26,105,105,26,50,10068,$a([252,476,690,816]),$a([2,7,17,0,7,16,34,0])]),$a(["full","23",$_.qrcode_v10to26,109,109,30,54,10916,$a([270,504,750,900]),$a([4,5,4,14,11,14,16,14])]),$a(["full","24",$_.qrcode_v10to26,113,113,28,54,11796,$a([300,560,810,960]),$a([6,4,6,14,11,16,30,2])]),$a(["full","25",$_.qrcode_v10to26,117,117,32,58,12708,$a([312,588,870,1050]),$a([8,4,8,13,7,22,22,13])]),$a(["full","26",$_.qrcode_v10to26,121,121,30,58,13652,$a([336,644,952,1110]),$a([10,2,19,4,28,6,33,4])]),$a(["full","27",$_.qrcode_v27to40,125,125,34,62,14628,$a([360,700,1020,1200]),$a([8,4,22,3,8,26,12,28])]),$a(["full","28",$_.qrcode_v27to40,129,129,26,50,15371,$a([390,728,1050,1260]),$a([3,10,3,23,4,31,11,31])]),$a(["full","29",$_.qrcode_v27to40,133,133,30,54,16411,$a([420,784,1140,1350]),$a([7,7,21,7,1,37,19,26])]),$a(["full","30",$_.qrcode_v27to40,137,137,26,52,17483,$a([450,812,1200,1440]),$a([5,10,19,10,15,25,23,25])]),$a(["full","31",$_.qrcode_v27to40,141,141,30,56,18587,$a([480,868,1290,1530]),$a([13,3,2,29,42,1,23,28])]),$a(["full","32",$_.qrcode_v27to40,145,145,34,60,19723,$a([510,924,1350,1620]),$a([17,0,10,23,10,35,19,35])]),$a(["full","33",$_.qrcode_v27to40,149,149,30,58,20891,$a([540,980,1440,1710]),$a([17,1,14,21,29,19,11,46])]),$a(["full","34",$_.qrcode_v27to40,153,153,34,62,22091,$a([570,1036,1530,1800]),$a([13,6,14,23,44,7,59,1])]),$a(["full","35",$_.qrcode_v27to40,157,157,30,54,23008,$a([570,1064,1590,1890]),$a([12,7,12,26,39,14,22,41])]),$a(["full","36",$_.qrcode_v27to40,161,161,24,50,24272,$a([600,1120,1680,1980]),$a([6,14,6,34,46,10,2,64])]),$a(["full","37",$_.qrcode_v27to40,165,165,28,54,25568,$a([630,1204,1770,2100]),$a([17,4,29,14,49,10,24,46])]),$a(["full","38",$_.qrcode_v27to40,169,169,32,58,26896,$a([660,1260,1860,2220]),$a([4,18,13,32,48,14,42,32])]),$a(["full","39",$_.qrcode_v27to40,173,173,26,54,28256,$a([720,1316,1950,2310]),$a([20,4,40,7,43,22,10,67])]),$a(["full","40",$_.qrcode_v27to40,177,177,30,58,29648,$a([750,1372,2040,2430]),$a([19,6,18,31,34,34,20,61])]),$a(["rmqr","R7x43",$_.qrcode_vR7x43,7,43,22,99,104,$a([99,7,99,10]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R7x59",$_.qrcode_vR7x59,7,59,20,40,171,$a([99,9,99,14]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R7x77",$_.qrcode_vR7x77,7,77,26,52,261,$a([99,12,99,22]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R7x99",$_.qrcode_vR7x99,7,99,24,50,358,$a([99,16,99,30]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R7x139",$_.qrcode_vR7x139,7,139,28,56,545,$a([99,24,99,44]),$a([-1,-1,1,0,-1,-1,2,0])]),$a(["rmqr","R9x43",$_.qrcode_vR9x43,9,43,22,99,170,$a([99,9,99,14]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R9x59",$_.qrcode_vR9x59,9,59,20,40,267,$a([99,12,99,22]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R9x77",$_.qrcode_vR9x77,9,77,26,52,393,$a([99,18,99,32]),$a([-1,-1,1,0,-1,-1,1,1])]),$a(["rmqr","R9x99",$_.qrcode_vR9x99,9,99,24,50,532,$a([99,24,99,44]),$a([-1,-1,1,0,-1,-1,2,0])]),$a(["rmqr","R9x139",$_.qrcode_vR9x139,9,139,28,56,797,$a([99,36,99,66]),$a([-1,-1,1,1,-1,-1,3,0])]),$a(["rmqr","R11x27",$_.qrcode_vR11x27,11,27,98,99,122,$a([99,8,99,10]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R11x43",$_.qrcode_vR11x43,11,43,22,99,249,$a([99,12,99,20]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R11x59",$_.qrcode_vR11x59,11,59,20,40,376,$a([99,16,99,32]),$a([-1,-1,1,0,-1,-1,1,1])]),$a(["rmqr","R11x77",$_.qrcode_vR11x77,11,77,26,52,538,$a([99,24,99,44]),$a([-1,-1,1,0,-1,-1,1,1])]),$a(["rmqr","R11x99",$_.qrcode_vR11x99,11,99,24,50,719,$a([99,32,99,60]),$a([-1,-1,1,1,-1,-1,1,1])]),$a(["rmqr","R11x139",$_.qrcode_vR11x139,11,139,28,56,1062,$a([99,48,99,90]),$a([-1,-1,2,0,-1,-1,3,0])]),$a(["rmqr","R13x27",$_.qrcode_vR13x27,13,27,98,99,172,$a([99,9,99,14]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R13x43",$_.qrcode_vR13x43,13,43,22,99,329,$a([99,14,99,28]),$a([-1,-1,1,0,-1,-1,1,0])]),$a(["rmqr","R13x59",$_.qrcode_vR13x59,13,59,20,40,486,$a([99,22,99,40]),$a([-1,-1,1,0,-1,-1,2,0])]),$a(["rmqr","R13x77",$_.qrcode_vR13x77,13,77,26,52,684,$a([99,32,99,56]),$a([-1,-1,1,1,-1,-1,1,1])]),$a(["rmqr","R13x99",$_.qrcode_vR13x99,13,99,24,50,907,$a([99,40,99,78]),$a([-1,-1,1,1,-1,-1,1,2])]),$a(["rmqr","R13x139",$_.qrcode_vR13x139,13,139,28,56,1328,$a([99,60,99,112]),$a([-1,-1,2,1,-1,-1,2,2])]),$a(["rmqr","R15x43",$_.qrcode_vR15x43,15,43,22,99,409,$a([99,18,99,36]),$a([-1,-1,1,0,-1,-1,1,1])]),$a(["rmqr","R15x59",$_.qrcode_vR15x59,15,59,20,40,596,$a([99,26,99,48]),$a([-1,-1,1,0,-1,-1,2,0])]),$a(["rmqr","R15x77",$_.qrcode_vR15x77,15,77,26,52,830,$a([99,36,99,72]),$a([-1,-1,1,1,-1,-1,2,1])]),$a(["rmqr","R15x99",$_.qrcode_vR15x99,15,99,24,50,1095,$a([99,48,99,88]),$a([-1,-1,2,0,-1,-1,4,0])]),$a(["rmqr","R15x139",$_.qrcode_vR15x139,15,139,28,56,1594,$a([99,72,99,130]),$a([-1,-1,2,1,-1,-1,1,4])]),$a(["rmqr","R17x43",$_.qrcode_vR17x43,17,43,22,99,489,$a([99,22,99,40]),$a([-1,-1,1,0,-1,-1,1,1])]),$a(["rmqr","R17x59",$_.qrcode_vR17x59,17,59,20,40,706,$a([99,32,99,60]),$a([-1,-1,2,0,-1,-1,2,0])]),$a(["rmqr","R17x77",$_.qrcode_vR17x77,17,77,26,52,976,$a([99,44,99,84]),$a([-1,-1,2,0,-1,-1,1,2])]),$a(["rmqr","R17x99",$_.qrcode_vR17x99,17,99,24,50,1283,$a([99,60,99,104]),$a([-1,-1,2,1,-1,-1,4,0])]),$a(["rmqr","R17x139",$_.qrcode_vR17x139,17,139,28,56,1860,$a([99,80,99,156]),$a([-1,-1,4,0,-1,-1,2,4])])]);var _57=$a([1,1,1,1,1,1,1,0]);var _58=$a([1,0,0,0,0,0,1,0]);var _59=$a([1,0,1,1,1,0,1,0]);$_.qrcode_fpat=$a([_57,_58,_59,_59,_59,_58,_57,$a([0,0,0,0,0,0,0,0])]);var _5C=$a([1,1,1,1,1,9,9,9]);var _5D=$a([1,0,0,0,1,9,9,9]);var _5F=$a([9,9,9,9,9,9,9,9]);$_.qrcode_fsubpat=$a([_5C,_5D,$a([1,0,1,0,1,9,9,9]),_5D,_5C,_5F,_5F,_5F]);var _5K=$a([9,9,9,9,9,9,9,9]);$_.qrcode_fcorpat=$a([$a([1,1,1,9,9,9,9,9]),$a([1,0,9,9,9,9,9,9]),$a([1,9,9,9,9,9,9,9]),_5K,_5K,_5K,_5K,_5K]);var _5M=$a([9,9,9,9,9,9,9,9]);$_.qrcode_fnullpat=$a([_5M,_5M,_5M,_5M,_5M,_5M,_5M,_5M]);var _5R=new Map([["full",$a([$_.qrcode_fpat,$_.qrcode_fpat,$_.qrcode_fpat,$_.qrcode_fnullpat])],["micro",$a([$_.qrcode_fpat,$_.qrcode_fnullpat,$_.qrcode_fnullpat,$_.qrcode_fnullpat])],["rmqr",$a([$_.qrcode_fpat,$_.qrcode_fcorpat,$_.qrcode_fcorpat,$_.qrcode_fsubpat])]]);$_.qrcode_fpatmap=_5R;var _5S=$a([1,1,1,1,1]);var _5T=$a([1,0,0,0,1]);$_.qrcode_algnpatfull=$a([_5S,_5T,$a([1,0,1,0,1]),_5T,_5S]);var _5W=$a([1,1,1,9,9]);var _5Y=$a([9,9,9,9,9]);$_.qrcode_algnpatrmqr=$a([_5W,$a([1,0,1,9,9]),_5W,_5Y,_5Y]);var _5b=$a([function(){$j-=2;$k[$j++]=0;$k[$j++]=8},function(){$j--;var _5a=$k[--$j];$k[$j++]=8;$k[$j++]=_5a-1}]);var _5d=$a([function(){$j-=2;$k[$j++]=1;$k[$j++]=8},function(){$j--;var _5c=$k[--$j];$k[$j++]=8;$k[$j++]=_5c-2}]);var _5f=$a([function(){$j-=2;$k[$j++]=2;$k[$j++]=8},function(){$j--;var _5e=$k[--$j];$k[$j++]=8;$k[$j++]=_5e-3}]);var _5h=$a([function(){$j-=2;$k[$j++]=3;$k[$j++]=8},function(){$j--;var _5g=$k[--$j];$k[$j++]=8;$k[$j++]=_5g-4}]);var _5j=$a([function(){$j-=2;$k[$j++]=4;$k[$j++]=8},function(){$j--;var _5i=$k[--$j];$k[$j++]=8;$k[$j++]=_5i-5}]);var _5l=$a([function(){$j-=2;$k[$j++]=5;$k[$j++]=8},function(){$j--;var _5k=$k[--$j];$k[$j++]=8;$k[$j++]=_5k-6}]);var _5n=$a([function(){$j-=2;$k[$j++]=7;$k[$j++]=8},function(){$j--;var _5m=$k[--$j];$k[$j++]=8;$k[$j++]=_5m-7}]);var _5p=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=8},function(){$j--;$k[$j-1]-=8;$k[$j++]=8}]);var _5r=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=7},function(){$j--;$k[$j-1]-=7;$k[$j++]=8}]);var _5t=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=5},function(){$j--;$k[$j-1]-=6;$k[$j++]=8}]);var _5v=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=4},function(){$j--;$k[$j-1]-=5;$k[$j++]=8}]);var _5x=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=3},function(){$j--;$k[$j-1]-=4;$k[$j++]=8}]);var _5z=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=2},function(){$j--;$k[$j-1]-=3;$k[$j++]=8}]);var _61=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=1},function(){$j--;$k[$j-1]-=2;$k[$j++]=8}]);var _63=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=0},function(){$j--;$k[$j-1]-=1;$k[$j++]=8}]);var _64=$a([_5b,_5d,_5f,_5h,_5j,_5l,_5n,_5p,_5r,_5t,_5v,_5x,_5z,_61,_63]);var _65=$a([function(){$j-=2;$k[$j++]=1;$k[$j++]=8}]);var _66=$a([function(){$j-=2;$k[$j++]=2;$k[$j++]=8}]);var _67=$a([function(){$j-=2;$k[$j++]=3;$k[$j++]=8}]);var _68=$a([function(){$j-=2;$k[$j++]=4;$k[$j++]=8}]);var _69=$a([function(){$j-=2;$k[$j++]=5;$k[$j++]=8}]);var _6A=$a([function(){$j-=2;$k[$j++]=6;$k[$j++]=8}]);var _6B=$a([function(){$j-=2;$k[$j++]=7;$k[$j++]=8}]);var _6C=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=8}]);var _6D=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=7}]);var _6E=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=6}]);var _6F=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=5}]);var _6G=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=4}]);var _6H=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=3}]);var _6I=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=2}]);var _6J=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=1}]);var _6K=$a([_65,_66,_67,_68,_69,_6A,_6B,_6C,_6D,_6E,_6F,_6G,_6H,_6I,_6J]);var _6N=$a([function(){$j-=2;$k[$j++]=11;$k[$j++]=3},function(){var _6L=$k[--$j];var _6M=$k[--$j];$k[$j++]=_6L-3;$k[$j++]=_6M-6}]);var _6Q=$a([function(){$j-=2;$k[$j++]=11;$k[$j++]=2},function(){var _6O=$k[--$j];var _6P=$k[--$j];$k[$j++]=_6O-4;$k[$j++]=_6P-6}]);var _6T=$a([function(){$j-=2;$k[$j++]=11;$k[$j++]=1},function(){var _6R=$k[--$j];var _6S=$k[--$j];$k[$j++]=_6R-5;$k[$j++]=_6S-6}]);var _6W=$a([function(){$j-=2;$k[$j++]=10;$k[$j++]=5},function(){var _6U=$k[--$j];var _6V=$k[--$j];$k[$j++]=_6U-6;$k[$j++]=_6V-2}]);var _6Z=$a([function(){$j-=2;$k[$j++]=10;$k[$j++]=4},function(){var _6X=$k[--$j];var _6Y=$k[--$j];$k[$j++]=_6X-6;$k[$j++]=_6Y-3}]);var _6c=$a([function(){$j-=2;$k[$j++]=10;$k[$j++]=3},function(){var _6a=$k[--$j];var _6b=$k[--$j];$k[$j++]=_6a-6;$k[$j++]=_6b-4}]);var _6f=$a([function(){$j-=2;$k[$j++]=10;$k[$j++]=2},function(){var _6d=$k[--$j];var _6e=$k[--$j];$k[$j++]=_6d-6;$k[$j++]=_6e-5}]);var _6i=$a([function(){$j-=2;$k[$j++]=10;$k[$j++]=1},function(){var _6g=$k[--$j];var _6h=$k[--$j];$k[$j++]=_6g-6;$k[$j++]=_6h-6}]);var _6l=$a([function(){$j-=2;$k[$j++]=9;$k[$j++]=5},function(){var _6j=$k[--$j];var _6k=$k[--$j];$k[$j++]=_6j-7;$k[$j++]=_6k-2}]);var _6o=$a([function(){$j-=2;$k[$j++]=9;$k[$j++]=4},function(){var _6m=$k[--$j];var _6n=$k[--$j];$k[$j++]=_6m-7;$k[$j++]=_6n-3}]);var _6r=$a([function(){$j-=2;$k[$j++]=9;$k[$j++]=3},function(){var _6p=$k[--$j];var _6q=$k[--$j];$k[$j++]=_6p-7;$k[$j++]=_6q-4}]);var _6u=$a([function(){$j-=2;$k[$j++]=9;$k[$j++]=2},function(){var _6s=$k[--$j];var _6t=$k[--$j];$k[$j++]=_6s-7;$k[$j++]=_6t-5}]);var _6x=$a([function(){$j-=2;$k[$j++]=9;$k[$j++]=1},function(){var _6v=$k[--$j];var _6w=$k[--$j];$k[$j++]=_6v-7;$k[$j++]=_6w-6}]);var _70=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=5},function(){var _6y=$k[--$j];var _6z=$k[--$j];$k[$j++]=_6y-8;$k[$j++]=_6z-2}]);var _73=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=4},function(){var _71=$k[--$j];var _72=$k[--$j];$k[$j++]=_71-8;$k[$j++]=_72-3}]);var _76=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=3},function(){var _74=$k[--$j];var _75=$k[--$j];$k[$j++]=_74-8;$k[$j++]=_75-4}]);var _79=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=2},function(){var _77=$k[--$j];var _78=$k[--$j];$k[$j++]=_77-8;$k[$j++]=_78-5}]);var _7C=$a([function(){$j-=2;$k[$j++]=8;$k[$j++]=1},function(){var _7A=$k[--$j];var _7B=$k[--$j];$k[$j++]=_7A-8;$k[$j++]=_7B-6}]);var _7E=new Map([["full",_64],["micro",_6K],["rmqr",$a([_6N,_6Q,_6T,_6W,_6Z,_6c,_6f,_6i,_6l,_6o,_6r,_6u,_6x,_70,_73,_76,_79,_7C])]]);$_.qrcode_formatfimmap=_7E;$_.qrcode_ecidfull=$a([1,0,3,2]);$_.qrcode_ecidrmqr=$a([-1,0,-1,1]);$_.qrcode_ecidmicrosym=$a([$a([0]),$a([1,2]),$a([3,4]),$a([5,6,7])]);var _7O=$a([function(){$k[$j-1]-=9;$k[$j++]=5},function(){var _7N=$k[--$j];$k[$j++]=5;$k[$j++]=_7N-9}]);var _7R=$a([function(){$k[$j-1]-=10;$k[$j++]=5},function(){var _7Q=$k[--$j];$k[$j++]=5;$k[$j++]=_7Q-10}]);var _7U=$a([function(){$k[$j-1]-=11;$k[$j++]=5},function(){var _7T=$k[--$j];$k[$j++]=5;$k[$j++]=_7T-11}]);var _7X=$a([function(){$k[$j-1]-=9;$k[$j++]=4},function(){var _7W=$k[--$j];$k[$j++]=4;$k[$j++]=_7W-9}]);var _7a=$a([function(){$k[$j-1]-=10;$k[$j++]=4},function(){var _7Z=$k[--$j];$k[$j++]=4;$k[$j++]=_7Z-10}]);var _7d=$a([function(){$k[$j-1]-=11;$k[$j++]=4},function(){var _7c=$k[--$j];$k[$j++]=4;$k[$j++]=_7c-11}]);var _7g=$a([function(){$k[$j-1]-=9;$k[$j++]=3},function(){var _7f=$k[--$j];$k[$j++]=3;$k[$j++]=_7f-9}]);var _7j=$a([function(){$k[$j-1]-=10;$k[$j++]=3},function(){var _7i=$k[--$j];$k[$j++]=3;$k[$j++]=_7i-10}]);var _7m=$a([function(){$k[$j-1]-=11;$k[$j++]=3},function(){var _7l=$k[--$j];$k[$j++]=3;$k[$j++]=_7l-11}]);var _7p=$a([function(){$k[$j-1]-=9;$k[$j++]=2},function(){var _7o=$k[--$j];$k[$j++]=2;$k[$j++]=_7o-9}]);var _7s=$a([function(){$k[$j-1]-=10;$k[$j++]=2},function(){var _7r=$k[--$j];$k[$j++]=2;$k[$j++]=_7r-10}]);var _7v=$a([function(){$k[$j-1]-=11;$k[$j++]=2},function(){var _7u=$k[--$j];$k[$j++]=2;$k[$j++]=_7u-11}]);var _7y=$a([function(){$k[$j-1]-=9;$k[$j++]=1},function(){var _7x=$k[--$j];$k[$j++]=1;$k[$j++]=_7x-9}]);var _81=$a([function(){$k[$j-1]-=10;$k[$j++]=1},function(){var _80=$k[--$j];$k[$j++]=1;$k[$j++]=_80-10}]);var _84=$a([function(){$k[$j-1]-=11;$k[$j++]=1},function(){var _83=$k[--$j];$k[$j++]=1;$k[$j++]=_83-11}]);var _87=$a([function(){$k[$j-1]-=9;$k[$j++]=0},function(){var _86=$k[--$j];$k[$j++]=0;$k[$j++]=_86-9}]);var _8A=$a([function(){$k[$j-1]-=10;$k[$j++]=0},function(){var _89=$k[--$j];$k[$j++]=0;$k[$j++]=_89-10}]);var _8D=$a([function(){$k[$j-1]-=11;$k[$j++]=0},function(){var _8C=$k[--$j];$k[$j++]=0;$k[$j++]=_8C-11}]);$_.qrcode_vimmap=$a([_7O,_7R,_7U,_7X,_7a,_7d,_7g,_7j,_7m,_7p,_7s,_7v,_7y,_81,_84,_87,_8A,_8D]);$_.e=1e4;$_.qrcode_mode0forceKB=$a([1,1,1,$_.e,$_.e,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);$_.qrcode_mode0forceA=$a([1,1,1,$_.e,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);$_.qrcode_mode0forceN=$a([1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]);$_.qrcode_mode0NbeforeB=$a([4,4,5,$_.e,$_.e,2,3,2,2,3,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]);$_.qrcode_modeBKbeforeB=$a([9,12,13,$_.e,$_.e,4,6,4,5,6,6,6,5,6,6,6,7,4,6,6,6,7,7,5,6,6,7,7,7,6,6,7,7,7,6,7,7,7,8]);$_.qrcode_modeBKbeforeA=$a([8,10,11,$_.e,$_.e,4,5,4,5,5,6,6,5,5,6,6,6,4,5,6,6,6,6,5,6,6,6,6,7,6,6,6,6,7,6,6,6,7,7]);$_.qrcode_modeBKbeforeN=$a([8,9,11,$_.e,$_.e,3,5,3,4,5,5,5,4,5,5,5,6,3,5,5,5,6,6,4,5,5,6,6,6,5,5,6,6,7,5,6,6,6,7]);$_.qrcode_modeBKbeforeE=$a([5,5,6,$_.e,$_.e,2,3,2,3,3,3,3,3,3,3,3,4,2,3,3,3,4,4,3,3,3,4,4,4,3,3,4,4,4,3,4,4,4,4]);$_.qrcode_modeBAbeforeK=$a([11,12,14,$_.e,$_.e,5,7,5,6,7,8,8,6,7,8,8,8,6,7,8,8,8,8,6,8,8,8,8,9,8,8,8,8,9,8,8,8,9,9]);$_.qrcode_modeBAbeforeB=$a([11,15,16,$_.e,$_.e,6,7,6,7,7,8,8,7,7,8,8,8,6,7,8,8,8,9,7,8,8,8,9,9,8,8,9,9,9,8,8,9,9,10]);$_.qrcode_modeBAbeforeN=$a([12,13,15,$_.e,$_.e,6,8,6,7,8,8,8,7,8,8,8,9,6,8,8,8,9,9,7,8,8,9,9,10,8,9,9,9,10,8,9,9,10,10]);$_.qrcode_modeBAbeforeE=$a([6,7,8,$_.e,$_.e,3,4,3,4,4,4,4,4,4,4,4,5,4,4,4,4,5,5,4,4,4,5,5,5,4,5,5,5,5,4,5,5,5,5]);$_.qrcode_modeBNbeforeK=$a([6,7,8,$_.e,$_.e,3,4,3,4,4,5,5,4,4,5,5,5,3,4,5,5,5,5,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5]);$_.qrcode_modeBNbeforeB=$a([6,8,9,$_.e,$_.e,3,4,3,4,4,5,5,4,4,5,5,5,3,4,5,5,5,5,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,6]);$_.qrcode_modeBNbeforeA=$a([6,7,8,$_.e,$_.e,3,4,3,4,4,5,5,4,4,5,5,5,4,4,5,5,5,5,4,5,5,5,5,5,5,5,5,5,6,5,5,5,5,6]);$_.qrcode_modeBNbeforeE=$a([3,4,4,$_.e,$_.e,2,3,2,2,3,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]);$_.qrcode_modeANbeforeA=$a([13,15,17,$_.e,5,7,9,7,8,9,9,9,8,9,9,9,11,7,9,9,9,11,11,8,9,9,10,11,11,9,10,11,11,11,9,11,11,11,11]);$_.qrcode_modeANbeforeB=$a([13,17,18,$_.e,$_.e,7,9,7,8,9,9,9,8,9,9,9,10,7,9,9,9,10,11,8,9,9,9,11,11,9,9,11,11,11,9,10,11,11,11]);$_.qrcode_modeANbeforeE=$a([7,8,9,$_.e,3,4,5,4,5,5,5,5,5,5,5,5,6,4,5,5,5,6,6,5,5,5,5,6,6,5,5,6,6,6,5,6,6,6,6]);var _9N=$a([function(){var _95=$k[--$j];var _96=$k[--$j];$k[$j++]=$f(_96+_95)%2},function(){var _97=$k[--$j];$k[$j-1]=_97%2},function(){$j--;var _9A=$k[--$j];$k[$j++]=_9A%3},function(){var _9B=$k[--$j];var _9C=$k[--$j];$k[$j++]=$f(_9C+_9B)%3},function(){var _9D=$k[--$j];var _9E=$k[--$j];$k[$j++]=(~~(_9D/2)+~~(_9E/3))%2},function(){var _9F=$k[--$j];var _9G=$k[--$j];var _9H=_9G*_9F;$k[$j++]=$f(_9H%2+_9H%3)},function(){var _9I=$k[--$j];var _9J=$k[--$j];var _9K=_9J*_9I;$k[$j++]=$f(_9K%2+_9K%3)%2},function(){var _9L=$k[--$j];var _9M=$k[--$j];$k[$j++]=$f(_9M*_9L%3+(_9M+_9L)%2)%2}]);$_.qrcode_maskfuncs=_9N;var _9R=new Map([["full",$a([0,1,2,3,4,5,6,7])],["micro",$a([1,4,6,7])],["rmqr",$a([4])]]);$_.qrcode_formatmaskbits=_9R;$_.qrcode_bchrem=function(){var _9S=$k[--$j];var _9T=$k[$j-1];$k[$j++]=$bs(_9S,_9T);for(;;){var _9U=$k[$j-1];if(_9U==0){break}var _9V=$k[$j-1];$k[$j++]=0;$k[$j++]=_9V;if(_9V>=65536){var _9W=$k[--$j];$k[$j-1]+=16;$k[$j++]=$flr(_9W/65536)}var _9Y=$k[$j-1];if(_9Y>=256){var _9Z=$k[--$j];$k[$j-1]+=8;$k[$j++]=$flr(_9Z/256)}var _9b=$k[$j-1];if(_9b>=16){var _9c=$k[--$j];$k[$j-1]+=4;$k[$j++]=$flr(_9c/16)}var _9e=$k[$j-1];if(_9e>=4){var _9f=$k[--$j];$k[$j-1]+=2;$k[$j++]=$flr(_9f/4)}var _9h=$k[--$j];if(_9h>=2){$k[$j-1]+=1}var _9j=$k[$j-1];var _9l=$k[$j-3];if($lt(_9j,_9l)){$j--;break}var _9m=$k[--$j];var _9n=$k[--$j];var _9o=$k[$j-1];var _9p=$k[$j-2];$k[$j++]=$bs(_9p,$f(_9m-_9o))^_9n}$r(3,1);$j-=2};$_.charvals=new Map;for(var _9q=0;_9q<=44;_9q+=1){$p($_.charvals,$g($_.qrcode_charmap,_9q),_9q)}$_.charvals=$_.charvals;$k[$j++]=Infinity;$k[$j++]=1;for(var _9u=0;_9u<255;_9u++){var _9v=$k[$j-1];var _9w=_9v*2;$k[$j++]=_9w;if(_9w>=256){var _9x=$k[--$j];$k[$j++]=_9x^285}}var _9y=$a();$_.rsalog=_9y;$_.rslog=$a(256);for(var _A0=1;_A0<=255;_A0+=1){$p($_.rslog,$g($_.rsalog,_A0),_A0)}$_.rslog=$_.rslog;$_.funcpatlayers=$a(6*12);for(var _A6=0;_A6<=11;_A6+=1){$_.j=_A6;for(var _A7=0;_A7<=5;_A7+=1){$_.i=_A7;$_.bits=0;for(var _A8=0;_A8<=7;_A8+=1){$_.m=_A8;$k[$j++]=$_.i;$k[$j++]=$_.j;if($g($_.qrcode_maskfuncs,$_.m)()===true){break}var _AD=$k[--$j];if(_AD==0){$_.bits=$_.bits|$bs(1,$_.m)}}$p($_.funcpatlayers,$_.j*6+$_.i,$_.bits)}}$_.funcpatlayers=$_.funcpatlayers;$_.fmtvalsfull=$a(32);$_.fmtvalsmicro=$a(32);for(var _AN=0;_AN<=31;_AN+=1){$k[$j++]=_AN;$k[$j++]=1335;$k[$j++]=10;$k[$j++]=_AN;$_.qrcode_bchrem();var _AO=$k[--$j];var _AP=$k[--$j];var _AQ=_AO|_AP*1024;$p($_.fmtvalsfull,_AP,_AQ^21522);$p($_.fmtvalsmicro,_AP,_AQ^17477)}$_.fmtvalsfull=$_.fmtvalsfull;$_.fmtvalsmicro=$_.fmtvalsmicro;$_.fmtvalsrmqr1=$a(64);$_.fmtvalsrmqr2=$a(64);$_.vervals=$a(34);for(var _AY=0;_AY<=63;_AY+=1){$k[$j++]=_AY;$k[$j++]=7973;$k[$j++]=12;$k[$j++]=_AY;$_.qrcode_bchrem();var _AZ=$k[--$j];var _Aa=$k[--$j];$p($_.fmtvalsrmqr1,_Aa,(_AZ|_Aa*4096)^129714);var _Ac=_AZ|_Aa*4096;$p($_.fmtvalsrmqr2,_Aa,_Ac^133755);$k[$j++]=_Ac;$k[$j++]=_Aa;if(_Aa>=7&&_Aa<=40){var _Ae=$k[--$j];var _Af=$k[--$j];$p($_.vervals,_Ae-7,_Af)}else{$j-=2}}$_.fmtvalsrmqr1=$_.fmtvalsrmqr1;$_.fmtvalsrmqr2=$_.fmtvalsrmqr2;$_.vervals=$_.vervals;$_.qrcode_rsprod=function(){var _Ak=$k[$j-1];if(_Ak!=0){var _Am=$k[$j-2];if(_Am!=0){var _Ao=$k[--$j];var _Aq=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_Ao)+$g($_.rslog,_Aq))%255)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.qrcode_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _Aw=0,_Ax=$_.ecpb;_Aw<_Ax;_Aw++){$k[$j++]=0}$_.coeffs=$a();for(var _B0=0,_Az=$_.ecpb-1;_B0<=_Az;_B0+=1){var _B3=$_.coeffs;$p(_B3,_B0+1,$g(_B3,_B0+1-1));$k[$j++]=$g($_.rsalog,_B0);for(var _B5=_B0;_B5>=1;_B5-=1){var _B6=$_.coeffs;var _B8=$k[$j-1];$k[$j++]=_B6;$k[$j++]=_B5;$k[$j++]=$g(_B6,_B5);$k[$j++]=_B8;$_.qrcode_rsprod();var _BA=$k[--$j];var _BB=$k[--$j];var _BD=$k[--$j];$p(_BD,_BB,$xo(_BA,$g($_.coeffs,_BB-1)))}var _BE=$_.coeffs;var _BG=$k[$j-1];$k[$j++]=_BE;$k[$j++]=0;$k[$j++]=$g(_BE,0);$k[$j++]=_BG;$_.qrcode_rsprod();var _BH=$k[--$j];var _BI=$k[--$j];var _BJ=$k[--$j];$p(_BJ,_BI,_BH);$j--}$k[$j++]=$G($_.coeffs,0,$_.coeffs.length-1)};$_.qrcode_coeffscachemax=20;$_.qrcode_coeffscachelimit=302;$k[$j++]=$_.qrcode_coeffscachemax;$k[$j++]=$_.qrcode_coeffscachelimit;bwipp_fifocache();var _BP=$k[--$j];$_.qrcode_coeffscache=_BP;$_.qrcode_coeffscachefetch=function(){var _BQ=$k[--$j];$_.ecpb=_BQ;var _BS=$g($_.qrcode_coeffscache,"fetch");$k[$j++]=$_.ecpb;$k[$j++]=function(){$_.qrcode_gencoeffs()};$k[$j++]=function(){var _BT=$k[--$j];$k[$j++]=_BT.length};if(_BS()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_qrcode.globals);$_.dontdraw=false;$_.format="unset";$_.version="unset";$_.eclevel="unset";$_.fixedeclevel=false;$_.parse=false;$_.parsefnc=false;$_.mask=-1;$_.suppresskanjimode=true;bwipp_processoptions();var _BU=$k[--$j];$_.options=_BU;var _BV=$k[--$j];$_.barcode=_BV;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.barcode.length==0){$k[$j++]="bwipp.qrcodeEmptyData#26910";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>3e4){$k[$j++]="bwipp.qrcodeinputTooLong#26914";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($ne($_.version,"unset")){if($eq($_.format,"unset")){$k[$j++]="full";if($_.version.length>0){if($eq($G($_.version,0,1),"M")){$k[$j-1]="micro"}if($eq($G($_.version,0,1),"R")){$k[$j-1]="rmqr"}}var _Bh=$k[--$j];$_.format=_Bh}}else{if($eq($_.format,"unset")){$_.format="full"}}if($ne($_.format,"full")&&($ne($_.format,"micro")&&$ne($_.format,"rmqr"))){$k[$j++]="bwipp.qrcodeInvalidFormat#26934";$k[$j++]="The format must be either full, micro or rmqr";bwipp_raiseerror()}if($eq($_.format,"rmqr")&&$eq($_.version,"unset")){$k[$j++]="bwipp.qrcodeRMQRwithoutVersion#26938";$k[$j++]="A version must be provided for RMQR";bwipp_raiseerror()}if($eq($_.eclevel,"unset")){var _Bq=$ne($_.format,"micro")?"M":"L";$_.eclevel=_Bq}if($ne($_.eclevel,"L")&&($ne($_.eclevel,"M")&&($ne($_.eclevel,"Q")&&$ne($_.eclevel,"H")))){$k[$j++]="bwipp.qrcodeInvalidEClevel#26947";$k[$j++]="Error correction level must be either L, M, Q, or H";bwipp_raiseerror()}if($_.mask!=-1&&$eq($_.format,"rmqr")){$k[$j++]="bwipp.qrcodeRMQRmask#26951";$k[$j++]="A mask cannot be supplied for RMQR";bwipp_raiseerror()}if($_.mask!=-1){var _C2=$eq($_.format,"full")?8:4;if($_.mask<1||$_.mask>_C2){$k[$j++]="bwipp.qrcodeBadMask#26956";$k[$j++]="An invalid mask was supplied";bwipp_raiseerror()}}$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true],["FNC1",$_.qrcode_fn1]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _C7=$k[--$j];$_.msg=_C7;$_.msglen=$_.msg.length;$_.fnc1first=false;if($_.msglen>0){if($g($_.msg,0)==$_.qrcode_fn1){$_.fnc1first=true;$k[$j++]=Infinity;var _CE=$G($_.msg,1,$_.msglen-1);for(var _CF=0,_CG=_CE.length;_CF<_CG;_CF++){var _CH=$g(_CE,_CF);$k[$j++]=_CH;if(_CH==37){var _CI=$k[$j-1];$k[$j++]=_CI}}$_.msg=$a();$_.msglen=$_.msg.length}}$_.tobin=function(){var _CK=$k[--$j];var _CL=$s(_CK);$k[$j++]=_CL;for(var _CN=0,_CM=_CL.length-1;_CN<=_CM;_CN+=1){var _CO=$k[$j-1];$p(_CO,_CN,48)}var _CP=$k[$j-1];$k[$j++]=_CP;var _CQ=$k[$j-3];var _CR=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _CT=$R($s(_CR.length),_CQ,2);$P(_CR,_CR.length-_CT.length,_CT)};$_.encA=function(){var _CU=$k[--$j];$_.in=_CU;if($_.fnc1first){$k[$j++]=Infinity;$F($_.in,function(){var _CX=$k[$j-1];if(_CX==$_.qrcode_fn1){$k[$j-1]=37}});$_.in=$a()}$k[$j++]=~~($_.in.length*11/2)+1;try{var _CZ=$k[--$j];$k[$j++]=$s(_CZ);var _Cb=false}catch(e){$k[$j++]=true}if(_Cb){$k[$j-1]="bwipp.qrcodeInputTooLarge#27011";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _Cc=$k[--$j];$_.out=_Cc;$_.k=0;$_.m=0;for(;;){if($_.k==$_.in.length){break}if($_.k<$_.in.length-1){$k[$j++]=$f($g($_.charvals,$g($_.in,$_.k))*45+$g($_.charvals,$g($_.in,$_.k+1)));$k[$j++]=11;$_.tobin();$_.k=$_.k+2}else{$k[$j++]=$g($_.charvals,$g($_.in,$_.k));$k[$j++]=6;$_.tobin();$_.k=$_.k+1}var _Cy=$k[--$j];$P($_.out,$_.m,_Cy);$_.m=_Cy.length+$_.m}$k[$j++]=$G($_.out,0,$_.m)};$_.encN=function(){var _D5=$k[--$j];$_.in=_D5;$k[$j++]=~~($_.in.length*10/3)+1;try{var _D7=$k[--$j];$k[$j++]=$s(_D7);var _D9=false}catch(e){$k[$j++]=true}if(_D9){$k[$j-1]="bwipp.qrcodeInputTooLarge#27032";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _DA=$k[--$j];$_.out=_DA;$_.k=0;$_.m=0;for(;;){if($_.k==$_.in.length){break}if($_.k<$_.in.length-2){var _DH=$G($_.in,$_.k,3);$k[$j++]=0;for(var _DI=0,_DJ=_DH.length;_DI<_DJ;_DI++){var _DL=$k[--$j];$k[$j++]=$f($g(_DH,_DI)+(_DL*10-48))}$k[$j++]=10;$_.tobin();$_.k=$_.k+3}else{if($_.k==$_.in.length-2){var _DR=$G($_.in,$_.k,2);$k[$j++]=0;for(var _DS=0,_DT=_DR.length;_DS<_DT;_DS++){var _DV=$k[--$j];$k[$j++]=$f($g(_DR,_DS)+(_DV*10-48))}$k[$j++]=7;$_.tobin();$_.k=$_.k+2}else{var _DZ=$G($_.in,$_.k,1);$k[$j++]=0;for(var _Da=0,_Db=_DZ.length;_Da<_Db;_Da++){var _Dd=$k[--$j];$k[$j++]=$f($g(_DZ,_Da)+(_Dd*10-48))}$k[$j++]=4;$_.tobin();$_.k=$_.k+1}}var _Df=$k[--$j];$P($_.out,$_.m,_Df);$_.m=_Df.length+$_.m}$k[$j++]=$G($_.out,0,$_.m)};$_.encB=function(){var _Dm=$k[--$j];$_.in=_Dm;if($_.fnc1first){$k[$j++]=Infinity;$F($_.in,function(){var _Dp=$k[$j-1];if(_Dp==$_.qrcode_fn1){$k[$j-1]=29}});$_.in=$a()}$k[$j++]=$_.in.length*8;try{var _Dr=$k[--$j];$k[$j++]=$s(_Dr);var _Dt=false}catch(e){$k[$j++]=true}if(_Dt){$k[$j-1]="bwipp.qrcodeInputTooLarge#27061";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _Du=$k[--$j];$_.out=_Du;for(var _Dx=0,_Dw=$_.in.length-1;_Dx<=_Dw;_Dx+=1){$_.k=_Dx;$k[$j++]=$i($g($_.in,$_.k));$k[$j++]=8;$_.tobin();$k[$j++]=$_.out;$k[$j++]=$_.k*8;var _E3=$k[$j-3];var _E4=$k[$j-1];var _E5=$k[$j-2];$j-=3;$P(_E5,_E4,_E3)}$k[$j++]=$_.out};$_.encK=function(){var _E7=$k[--$j];$_.in=_E7;$k[$j++]=~~($_.in.length/2)*13;try{var _E9=$k[--$j];$k[$j++]=$s(_E9);var _EB=false}catch(e){$k[$j++]=true}if(_EB){$k[$j-1]="bwipp.qrcodeInputTooLarge#27075";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _EC=$k[--$j];$_.out=_EC;$_.k=0;$_.m=0;for(;;){if($_.k==$_.in.length){break}var _EL=$f($g($_.in,$_.k)*256+$g($_.in,$_.k+1));var _EM=_EL<57408?33088:49472;var _EO=$f(_EL-_EM);$k[$j++]=$f($flr(_EO/256)*192+(_EO&255));$k[$j++]=13;$_.tobin();var _EP=$k[--$j];$P($_.out,$_.m,_EP);$_.m=_EP.length+$_.m;$_.k=$_.k+2}$k[$j++]=$_.out};$_.encE=function(){var _EV=$k[--$j];var _EX=-$g(_EV,0)-1e6;$k[$j++]=_EX;if(_EX<=127){$k[$j++]=8;$_.tobin()}else{var _EY=$k[$j-1];if(_EY<=16383){$k[$j-1]+=32768;$k[$j++]=16;$_.tobin()}else{$k[$j-1]+=12582912;$k[$j++]=24;$_.tobin()}}};$_.addtobits=function(){var _Eb=$k[$j-1];if(_Eb.length+$_.j>$_.bits.length){$k[$j-1]="bwipp.qrcodeNoValidSymbol#27102";$k[$j++]="Maximum length exceeded or invalid content";bwipp_raiseerror()}var _Ee=$k[--$j];$P($_.bits,$_.j,_Ee);$_.j=_Ee.length+$_.j};$k[$j++]=Infinity;for(var _Ej=0,_Ek=$_.msglen;_Ej<_Ek;_Ej++){$k[$j++]=0}$k[$j++]=0;$_.numNs=$a();$k[$j++]=Infinity;for(var _Em=0,_En=$_.msglen;_Em<_En;_Em++){$k[$j++]=0}$k[$j++]=0;$_.numAs=$a();$k[$j++]=Infinity;for(var _Ep=0,_Eq=$_.msglen;_Ep<_Eq;_Ep++){$k[$j++]=0}$k[$j++]=0;$_.numAorNs=$a();$k[$j++]=Infinity;for(var _Es=0,_Et=$_.msglen;_Es<_Et;_Es++){$k[$j++]=0}$k[$j++]=0;$_.numBs=$a();$k[$j++]=Infinity;for(var _Ev=0,_Ew=$_.msglen;_Ev<_Ew;_Ev++){$k[$j++]=0}$k[$j++]=0;$_.numKs=$a();$k[$j++]=Infinity;for(var _Ey=0,_Ez=$_.msglen;_Ey<_Ez;_Ey++){$k[$j++]=0}$k[$j++]=9999;$_.nextNs=$a();$k[$j++]=Infinity;for(var _F1=0,_F2=$_.msglen;_F1<_F2;_F1++){$k[$j++]=0}$k[$j++]=9999;$_.nextBs=$a();$k[$j++]=Infinity;for(var _F4=0,_F5=$_.msglen;_F4<_F5;_F4++){$k[$j++]=0}$k[$j++]=9999;$_.nextAs=$a();$k[$j++]=Infinity;for(var _F7=0,_F8=$_.msglen;_F7<_F8;_F7++){$k[$j++]=0}$k[$j++]=9999;$_.nextKs=$a();$_.isECI=$a($_.msglen);for(var _FC=$_.msglen-1;_FC>=0;_FC-=1){$_.i=_FC;$_.barchar=$g($_.msg,$_.i);if($has($_.qrcode_Kexcl,$_.barchar)&&!$_.suppresskanjimode){var _FP=$_.i+1<$_.msglen?$f($_.barchar*256+$g($_.msg,$_.i+1)):0;var _FQ=_FP&255;if((_FP>=33088&&_FP<=40956||_FP>=57408&&_FP<=60351)&&(_FQ>=64&&_FQ<=252&&_FQ!=127)){$p($_.nextKs,$_.i,0);$p($_.numKs,$_.i,$g($_.numKs,$_.i+2)+1)}else{$p($_.nextKs,$_.i,$g($_.nextKs,$_.i+1)+1)}}else{$p($_.nextKs,$_.i,$g($_.nextKs,$_.i+1)+1)}if($has($_.qrcode_Nexcl,$_.barchar)){$p($_.nextNs,$_.i,0);$p($_.numNs,$_.i,$g($_.numNs,$_.i+1)+1);$p($_.numAorNs,$_.i,$g($_.numAorNs,$_.i+1)+1)}else{$p($_.nextNs,$_.i,$g($_.nextNs,$_.i+1)+1)}if($has($_.qrcode_Aexcl,$_.barchar)){$p($_.nextAs,$_.i,0);$p($_.numAs,$_.i,$g($_.numAs,$_.i+1)+1);$p($_.numAorNs,$_.i,$g($_.numAorNs,$_.i+1)+1)}else{$p($_.nextAs,$_.i,$g($_.nextAs,$_.i+1)+1)}$p($_.isECI,$_.i,$_.barchar<=-1e6)}for(var _GP=0,_GO=$_.msglen-1;_GP<=_GO;_GP+=1){$_.i=_GP;if($g($_.numKs,$_.i)>0){$p($_.numKs,$_.i+1,0);$p($_.nextKs,$_.i+1,$g($_.nextKs,$_.i+1)+1)}}for(var _Gb=$_.msglen-1;_Gb>=0;_Gb-=1){$_.i=_Gb;if($f($g($_.numNs,$_.i)+($g($_.numAs,$_.i)+$g($_.numKs,$_.i)))==0&&!$g($_.isECI,$_.i)){$p($_.nextBs,$_.i,0);$p($_.numBs,$_.i,$g($_.numBs,$_.i+1)+1)}else{$p($_.nextBs,$_.i,$g($_.nextBs,$_.i+1)+1)}}$_.KbeforeB=function(){var _H1=$k[--$j];$k[$j++]=$ge($_.numK,$g(_H1,$_.ver))&&$g($_.nextBs,$f($_.numK*2+$_.i))==0};$_.KbeforeA=function(){var _H9=$k[--$j];$k[$j++]=$ge($_.numK,$g(_H9,$_.ver))&&$g($_.nextAs,$f($_.numK*2+$_.i))==0};$_.KbeforeN=function(){var _HH=$k[--$j];$k[$j++]=$ge($_.numK,$g(_HH,$_.ver))&&$g($_.nextNs,$f($_.numK*2+$_.i))==0};$_.KbeforeE=function(){var _HP=$k[--$j];$k[$j++]=$ge($_.numK,$g(_HP,$_.ver))&&$f($_.numK*2+$_.i)==$_.msglen};$_.AbeforeK=function(){var _HW=$k[--$j];$k[$j++]=$ge($_.numA,$g(_HW,$_.ver))&&$g($_.nextKs,$f($_.numA+$_.i))==0};$_.AbeforeB=function(){var _He=$k[--$j];$k[$j++]=$ge($_.numA,$g(_He,$_.ver))&&$g($_.nextBs,$f($_.numA+$_.i))==0};$_.AbeforeN=function(){var _Hm=$k[--$j];$k[$j++]=$ge($_.numA,$g(_Hm,$_.ver))&&$g($_.nextNs,$f($_.numA+$_.i))==0};$_.AbeforeE=function(){var _Hu=$k[--$j];$k[$j++]=$ge($_.numA,$g(_Hu,$_.ver))&&$f($_.numA+$_.i)==$_.msglen};$_.NbeforeK=function(){var _I1=$k[--$j];$k[$j++]=$ge($_.numN,$g(_I1,$_.ver))&&$g($_.nextKs,$f($_.numN+$_.i))==0};$_.NbeforeB=function(){var _I9=$k[--$j];$k[$j++]=$ge($_.numN,$g(_I9,$_.ver))&&$g($_.nextBs,$f($_.numN+$_.i))==0};$_.NbeforeA=function(){var _IH=$k[--$j];$k[$j++]=$ge($_.numN,$g(_IH,$_.ver))&&$g($_.nextAs,$f($_.numN+$_.i))==0};$_.NbeforeE=function(){var _IP=$k[--$j];$k[$j++]=$ge($_.numN,$g(_IP,$_.ver))&&$f($_.numN+$_.i)==$_.msglen};$_.AorNbeforeB=function(){var _IW=$k[--$j];$k[$j++]=$ge($_.numAorN,$g(_IW,$_.ver))&&$g($_.nextBs,$f($_.numAorN+$_.i))==0};$_.AorNbeforeE=function(){var _Ie=$k[--$j];$k[$j++]=$ge($_.numAorN,$g(_Ie,$_.ver))&&$f($_.numAorN+$_.i)==$_.msglen};$_.nextNslt=function(){if($g($_.nextNs,$_.i)>=$_.msglen){$k[$j-1]=true}else{var _It=$g($_.numNs,$f($g($_.nextNs,$_.i)+$_.i));var _Iu=$k[--$j];$k[$j++]=$lt(_It,$g(_Iu,$_.ver))}};if($ne($_.version,"unset")){var _Iz=$g($_.qrcode_versetmap,$_.format);var _J0=$_.version;var _J1=$has(_Iz,_J0);$k[$j++]=_Iz;$k[$j++]=_J0;if(!_J1){$j-=2;if($eq($_.format,"full")){$k[$j++]="bwipp.qrcodeInvalidFullVersion#27201";$k[$j++]="Valid versions for QR Code symbols are 1 to 40";bwipp_raiseerror()}else{if($eq($_.format,"micro")){$k[$j++]="bwipp.qrcodeInvalidMicroVersion#27204";$k[$j++]="Valid versions for Micro QR Code symbols are M1 to M4";bwipp_raiseerror()}else{$k[$j++]="bwipp.qrcodeInvalidRMQRversion#27206";$k[$j++]="Invalid version for an RMQR symbol";bwipp_raiseerror()}}}var _J4=$k[--$j];var _J5=$k[--$j];$k[$j++]=Infinity;$k[$j++]=$g(_J5,_J4);var _J9=$a();$_.verset=_J9}else{if($eq($_.format,"full")){$_.verset=$_.qrcode_versetfull}if($eq($_.format,"micro")){$_.verset=$_.qrcode_versetmicro}}$k[$j++]=Infinity;for(var _JC=0;_JC<39;_JC++){$k[$j++]=-1}$_.msgbits=$a();var _JD=$_.verset;for(var _JE=0,_JF=_JD.length;_JE<_JF;_JE++){$_.ver=$g(_JD,_JE);$_.mode=-1;$_.seqok=true;$k[$j++]=Infinity;$_.i=0;for(;;){if($_.i>=$_.msglen){break}$_.numK=$g($_.numKs,$_.i);$_.numB=$g($_.numBs,$_.i);$_.numA=$g($_.numAs,$_.i);$_.numN=$g($_.numNs,$_.i);$_.numAorN=$g($_.numAorNs,$_.i);$_.eci=$g($_.isECI,$_.i);if($_.ver==$_.qrcode_vM1&&$_.numA>=1){$_.seqok=false;break}if($_.ver==$_.qrcode_vM1&&$_.numB>=1){$_.seqok=false;break}if($_.ver==$_.qrcode_vM1&&$_.numK>=1){$_.seqok=false;break}if($_.ver==$_.qrcode_vM1&&$_.eci){$_.seqok=false;break}if($_.ver==$_.qrcode_vM2&&$_.numB>=1){$_.seqok=false;break}if($_.ver==$_.qrcode_vM2&&$_.numK>=1){$_.seqok=false;break}if($_.ver==$_.qrcode_vM2&&$_.eci){$_.seqok=false;break}if($_.ver==$_.qrcode_vM3&&$_.eci){$_.seqok=false;break}if($_.ver==$_.qrcode_vM4&&$_.eci){$_.seqok=false;break}for(;;){if($_.eci){$k[$j++]=$_.qrcode_E;break}if($_.mode==-1){$k[$j++]=$_.qrcode_mode0forceKB;$_.KbeforeA();var _Jv=$k[--$j];if(_Jv){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_mode0forceKB;$_.KbeforeN();var _Jw=$k[--$j];if(_Jw){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_modeBKbeforeE;$_.KbeforeB();var _Jx=$k[--$j];if(_Jx){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_mode0forceKB;$_.KbeforeE();var _Jy=$k[--$j];if(_Jy){$k[$j++]=$_.qrcode_K;break}if($_.numK>=1){$k[$j++]=$_.qrcode_B;break}$k[$j++]=$_.qrcode_mode0NbeforeB;$_.NbeforeK();var _K0=$k[--$j];if(_K0){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_mode0NbeforeB;$_.NbeforeB();var _K1=$k[--$j];if(_K1){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_mode0forceKB;$_.NbeforeB();var _K2=$k[--$j];if(_K2){$k[$j++]=$_.qrcode_B;break}$k[$j++]=$_.qrcode_modeANbeforeE;$_.NbeforeA();var _K3=$k[--$j];if(_K3){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_mode0forceN;$_.NbeforeE();var _K4=$k[--$j];if(_K4){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeBAbeforeE;$_.AbeforeK();var _K5=$k[--$j];if(_K5){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_modeBAbeforeE;$_.AorNbeforeB();var _K6=$k[--$j];if(_K6){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_mode0forceA;$_.AorNbeforeE();var _K7=$k[--$j];if(_K7){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_B;break}if($_.mode==$_.qrcode_B){$k[$j++]=$_.qrcode_modeBKbeforeB;$_.KbeforeB();var _K9=$k[--$j];if(_K9){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_modeBKbeforeA;$_.KbeforeA();var _KA=$k[--$j];if(_KA){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_modeBKbeforeN;$_.KbeforeN();var _KB=$k[--$j];if(_KB){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_modeBKbeforeE;$_.KbeforeE();var _KC=$k[--$j];if(_KC){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_modeBAbeforeK;$_.AbeforeK();var _KD=$k[--$j];if(_KD){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_modeBAbeforeB;$_.AbeforeB();var _KE=$k[--$j];if(_KE){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_modeBAbeforeN;$_.AbeforeN();var _KF=$k[--$j];if(_KF){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_modeBAbeforeE;$_.AbeforeE();var _KG=$k[--$j];if(_KG){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_modeBNbeforeK;$_.NbeforeK();var _KH=$k[--$j];if(_KH){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeBNbeforeB;$_.NbeforeB();var _KI=$k[--$j];if(_KI){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeBNbeforeA;$_.NbeforeA();var _KJ=$k[--$j];if(_KJ){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeBNbeforeE;$_.NbeforeE();var _KK=$k[--$j];if(_KK){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeBAbeforeE;$_.AorNbeforeE();var _KO=$k[--$j];if(_KO&&$le($_.numAorN,$g($_.qrcode_modeBAbeforeN,$_.ver))){$k[$j++]=$_.qrcode_modeBNbeforeA;$_.nextNslt();var _KP=$k[--$j];if(_KP){$k[$j++]=$_.qrcode_A;break}}$k[$j++]=$_.qrcode_B;break}if($_.mode==$_.qrcode_A){if($_.numK>=1){$k[$j++]=$_.qrcode_K;break}if($_.numB>=1){$k[$j++]=$_.qrcode_B;break}$k[$j++]=$_.qrcode_modeANbeforeA;$_.NbeforeA();var _KT=$k[--$j];if(_KT){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeANbeforeB;$_.NbeforeB();var _KU=$k[--$j];if(_KU){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_modeANbeforeE;$_.NbeforeE();var _KV=$k[--$j];if(_KV){$k[$j++]=$_.qrcode_N;break}if($_.numA>=1||$_.numN>=1){$k[$j++]=$_.qrcode_A;break}$k[$j++]=$_.qrcode_B;break}if($_.mode==$_.qrcode_N){if($_.numK>=1){$k[$j++]=$_.qrcode_K;break}if($_.numB>=1){$k[$j++]=$_.qrcode_B;break}if($_.numA>=1){$k[$j++]=$_.qrcode_A;break}if($_.numN>=1){$k[$j++]=$_.qrcode_N;break}$k[$j++]=$_.qrcode_B;break}if($_.mode==$_.qrcode_K){if($_.numB>=1){$k[$j++]=$_.qrcode_B;break}if($_.numA>=1){$k[$j++]=$_.qrcode_A;break}if($_.numN>=1){$k[$j++]=$_.qrcode_N;break}if($_.numK>=1){$k[$j++]=$_.qrcode_K;break}$k[$j++]=$_.qrcode_B;break}}var _Ki=$k[$j-1];if(_Ki==$_.qrcode_K&&$_.fnc1first){$k[$j-1]=$_.qrcode_B}var _Kk=$k[$j-1];if(_Kk==$_.mode){$j--;var _Kp=$_.mode==$_.qrcode_K?2:1;$_.dat=$G($_.msg,$_.i,_Kp);var _Ks=$k[--$j];$k[$j++]=Infinity;$q(_Ks);$q($_.dat);var _Ku=$a();$k[$j++]=_Ku}else{var _Kv=$k[--$j];$_.mode=_Kv;if($_.mode==$_.qrcode_K){$k[$j++]=$_.qrcode_K;$k[$j++]=$G($_.msg,$_.i,$_.numK*2)}if($_.mode==$_.qrcode_B){$k[$j++]=$_.qrcode_B;$k[$j++]=$G($_.msg,$_.i,$_.numB)}if($_.mode==$_.qrcode_A){$k[$j++]=$_.qrcode_A;$k[$j++]=$G($_.msg,$_.i,$_.numA)}if($_.mode==$_.qrcode_N){$k[$j++]=$_.qrcode_N;$k[$j++]=$G($_.msg,$_.i,$_.numN)}if($_.mode==$_.qrcode_E){$_.mode=-1;$k[$j++]=$_.qrcode_E;$k[$j++]=$G($_.msg,$_.i,1)}var _LK=$k[--$j];$_.dat=_LK;$k[$j++]=_LK}$_.i=$_.i+$_.dat.length}if($_.seqok){$as($a($m()));var _LQ=$k[--$j];$k[$j-1]=_LQ}else{$l();$k[$j++]=-1}var _LS=$k[--$j];$_.seq=_LS;for(;;){if($_.seq==-1){break}$_.bits=$s(23648);$_.j=0;if($_.fnc1first){if($_.ver<$_.qrcode_vR7x43){$k[$j++]="0101"}else{$k[$j++]="101"}$_.addtobits()}$_.abort=false;for(var _LZ=0,_LY=$_.seq.length-1;_LZ<=_LY;_LZ+=2){$_.i=_LZ;$_.mode=$g($_.seq,$_.i);$k[$j++]=$g($g($_.qrcode_mids,$_.ver),$_.mode);$_.addtobits();$_.chars=$g($_.seq,$_.i+1);var _Ln=$_.chars.length;if($_.mode==$_.qrcode_K){_Ln=~~(_Ln/2)}$_.charslen=_Ln;if($_.mode!=$_.qrcode_E){$_.cclen=$g($g($_.qrcode_cclens,$_.ver),$_.mode);if($_.charslen>=$pow(2,$_.cclen)){$_.abort=true;break}$k[$j++]=$_.charslen;$k[$j++]=$_.cclen;$_.tobin();$_.addtobits()}var _M1=$_[$g($_.qrcode_encfuncs,$_.mode)];$k[$j++]=$_.chars;if(_M1()===true){break}$_.addtobits()}if($_.abort){break}$_.bits=$G($_.bits,0,$_.j);$p($_.msgbits,$_.ver,$_.bits);break}}if($has($_.options,"debugbitseqs")){$k[$j++]="bwipp.debugbitseqs#27361";$k[$j++]=$_.msgbits;bwipp_raiseerror()}$k[$j++]="eclval";$X("LMQH",$_.eclevel);$j--;var _MD=$k[--$j];$j--;var _MF=_MD.length;$j--;var _MI=$k[--$j];$_[_MI]=_MF;for(var _MK=0,_MJ=$_.qrcode_metrics.length-1;_MK<=_MJ;_MK+=1){$_.i=_MK;$_.m=$g($_.qrcode_metrics,$_.i);$_.frmt=$g($_.m,0);$_.vers=$g($_.m,1);$_.vergrp=$g($_.m,2);$_.verind=$_.i-44;$_.rows=$g($_.m,3);$_.cols=$g($_.m,4);$_.asp2=$g($_.m,5);$_.asp3=$g($_.m,6);$_.nmod=$g($_.m,7);$_.ncws=~~($_.nmod/8);$_.rbit=$_.nmod%8;$_.lc4b=false;if($eq($_.vers,"M1")||$eq($_.vers,"M3")){$_.ncws=$_.ncws+1;$_.rbit=0;$_.lc4b=true}$_.ecws=$g($g($_.m,8),$_.eclval);$_.dcws=$f($_.ncws-$_.ecws);var _Mr=$_.lc4b?4:0;$_.dmod=$f($_.dcws*8-_Mr);$_.ecb1=$g($g($_.m,9),$_.eclval*2);$_.ecb2=$g($g($_.m,9),$_.eclval*2+1);$_.okay=true;if($ne($_.format,$_.frmt)){$_.okay=false}if($eq($_.frmt,"micro")&&$_.fnc1first){$_.okay=false}if($ne($_.version,"unset")&&$ne($_.version,$_.vers)){$_.okay=false}if($_.ecb1==-1||$_.ecb2==-1){$_.okay=false}$_.verbits=$g($_.msgbits,$_.vergrp);if($_.verbits==-1){$_.okay=false}else{if($_.verbits.length>$_.dmod){$_.okay=false}}$_.term=$G("000000000",0,$g($_.qrcode_termlens,$_.vergrp));if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.qrcodeNoValidSymbol#27408";$k[$j++]="Maximum length exceeded or invalid content";bwipp_raiseerror()}$_.format=$_.frmt;$_.version=$_.vers;$_.msgbits=$_.verbits;if(!$_.fixedeclevel){for(var _NP=$_.eclval+1;_NP<=3;_NP+=1){$_.eclval_=_NP;$_.ecws_=$g($g($_.m,8),$_.eclval_);$_.dcws_=$f($_.ncws-$_.ecws_);var _NY=$_.lc4b?4:0;$_.dmod_=$f($_.dcws_*8-_NY);$_.ecb1_=$g($g($_.m,9),$_.eclval_*2);$_.ecb2_=$g($g($_.m,9),$_.eclval_*2+1);$_.okay=true;if($_.ecb1_==-1||$_.ecb2_==-1){$_.okay=false}if($_.msgbits.length>$_.dmod_){$_.okay=false}if($_.okay){$_.eclval=$_.eclval_;$_.dcws=$_.dcws_;$_.dmod=$_.dmod_;$_.ecb1=$_.ecb1_;$_.ecb2=$_.ecb2_}}}$_.dcpb=~~($_.dcws/$f($_.ecb1+$_.ecb2));$_.ecpb=~~($_.ncws/$f($_.ecb1+$_.ecb2))-$_.dcpb;var _Ny=$_.term;var _Nz=$_.dmod;var _O0=$_.msgbits;var _O1=$_.term;var _O2=_O1.length;var _O3=$f(_Nz-_O0.length);if($f(_Nz-_O0.length)>_O1.length){var _=_O2;_O2=_O3;_O3=_}$_.term=$G(_Ny,0,_O3);var _O7=$s($_.msgbits.length+$_.term.length);$P(_O7,0,$_.msgbits);$P(_O7,$_.msgbits.length,$_.term);$_.msgbits=_O7;$_.pad=$s($_.dmod);for(var _OF=0,_OE=$_.pad.length-1;_OF<=_OE;_OF+=1){$p($_.pad,_OF,48)}$P($_.pad,0,$_.msgbits);$_.padnum=0;var _OM=$_.lc4b?5:1;for(var _OO=$ceil($_.msgbits.length/8)*8,_ON=$f($_.dmod-_OM);_OO<=_ON;_OO+=8){$P($_.pad,_OO,$g($_.qrcode_padstrs,$_.padnum));$_.padnum=($_.padnum+1)%2}$_.cws=$a($_.dcws);for(var _OX=0,_OW=$_.cws.length-1;_OX<=_OW;_OX+=1){$_.c=_OX;$_.bpcw=8;if($_.lc4b&&$_.c==$_.cws.length-1){$_.bpcw=4}$_.cwb=$G($_.pad,$_.c*8,$_.bpcw);$_.cw=0;for(var _Oh=0,_Og=$_.bpcw-1;_Oh<=_Og;_Oh+=1){$_.i=_Oh;$_.cw=$f($_.cw+$pow(2,$_.bpcw-$_.i-1)*($g($_.cwb,$_.i)-48))}$p($_.cws,$_.c,$_.cw)}if($_.lc4b){var _Os=$_.cws;var _Ot=$_.cws;$p(_Os,_Ot.length-1,$g(_Os,_Ot.length-1)*16)}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#27487";$k[$j++]=$_.cws;bwipp_raiseerror()}$k[$j++]="coeffs";$k[$j++]=$_.ecpb;$_.qrcode_coeffscachefetch();var _Oz=$k[--$j];var _P0=$k[--$j];$_[_P0]=_Oz;$_.rscodes=function(){var _P1=$k[--$j];$_.rscws=_P1;$_.lfsr=$a($_.ecpb);var _P4=$_.rscws;var _P5=$_.coeffs;var _P6=$_.ecpb;var _P7=$_.lfsr;$k[$j++]=function(){var _P9=$k[--$j];$k[$j++]=$g($_.rscws,_P9)};$k[$j++]=_P4.length;$k[$j++]=_P5;$k[$j++]=_P6;$k[$j++]=_P7;$k[$j++]=$_.qrcode_rsprod;bwipp_rsecbinary();$k[$j++]=$_.lfsr};$_.dcwsb=$a($f($_.ecb1+$_.ecb2));$_.ecwsb=$a($f($_.ecb1+$_.ecb2));for(var _PK=0,_PJ=$_.ecb1-1;_PK<=_PJ;_PK+=1){$_.i=_PK;$p($_.dcwsb,$_.i,$G($_.cws,$_.i*$_.dcpb,$_.dcpb));$k[$j++]=$_.ecwsb;$k[$j++]=$_.i;$k[$j++]=$g($_.dcwsb,$_.i);$_.rscodes();var _PX=$k[--$j];var _PY=$k[--$j];var _PZ=$k[--$j];$p(_PZ,_PY,_PX)}for(var _Pc=0,_Pb=$_.ecb2-1;_Pc<=_Pb;_Pc+=1){$_.i=_Pc;$p($_.dcwsb,$f($_.ecb1+$_.i),$G($_.cws,$f($_.ecb1*$_.dcpb+$_.i*($_.dcpb+1)),$_.dcpb+1));var _Pt=$g($_.dcwsb,$f($_.ecb1+$_.i));$k[$j++]=$_.ecwsb;$k[$j++]=$f($_.ecb1+$_.i);$k[$j++]=_Pt;$_.rscodes();var _Pu=$k[--$j];var _Pv=$k[--$j];var _Pw=$k[--$j];$p(_Pw,_Pv,_Pu)}$_.cws=$a($_.ncws);$_.cw=0;for(var _Q1=0,_Q0=$_.dcpb;_Q1<=_Q0;_Q1+=1){$_.i=_Q1;for(var _Q5=0,_Q4=$f($_.ecb1+$_.ecb2)-1;_Q5<=_Q4;_Q5+=1){$_.j=_Q5;if($_.i<$g($_.dcwsb,$_.j).length){$p($_.cws,$_.cw,$g($g($_.dcwsb,$_.j),$_.i));$_.cw=$_.cw+1}}}for(var _QK=0,_QJ=$_.ecpb-1;_QK<=_QJ;_QK+=1){$_.i=_QK;for(var _QO=0,_QN=$f($_.ecb1+$_.ecb2)-1;_QO<=_QN;_QO+=1){$_.j=_QO;$p($_.cws,$_.cw,$g($g($_.ecwsb,$_.j),$_.i));$_.cw=$_.cw+1}}if($_.rbit>0){$_.pad=$a($_.cws.length+1);$P($_.pad,0,$_.cws);$p($_.pad,$_.pad.length-1,0);$_.cws=$_.pad}if($_.lc4b){var _Qg=$_.cws;var _Qh=$_.dcws;$p(_Qg,_Qh-1,$flr($g(_Qg,_Qh-1)/16));for(var _Qm=$_.dcws-1,_Ql=$_.ncws-2;_Qm<=_Ql;_Qm+=1){$_.i=_Qm;$p($_.cws,$_.i,($g($_.cws,$_.i)&15)*16);$p($_.cws,$_.i,$flr($g($_.cws,$_.i+1)/16)&15|$g($_.cws,$_.i))}$p($_.cws,$_.ncws-1,($g($_.cws,$_.ncws-1)&15)*16)}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#27577";$k[$j++]=$_.cws;bwipp_raiseerror()}$k[$j++]=Infinity;for(var _RA=0,_RB=$_.rows*$_.cols;_RA<_RB;_RA++){$k[$j++]=-1}$_.pixs=$a();$_.qmv=function(){var _RD=$k[--$j];var _RE=$k[--$j];$k[$j++]=$f(_RE+_RD*$_.cols)};if($eq($_.format,"full")){for(var _RI=8,_RH=$_.cols-9;_RI<=_RH;_RI+=1){$_.i=_RI;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=6;$_.qmv();var _RM=$k[--$j];var _RN=$k[--$j];$p(_RN,_RM,($_.i+1)%2);$k[$j++]=$_.pixs;$k[$j++]=6;$k[$j++]=$_.i;$_.qmv();var _RR=$k[--$j];var _RS=$k[--$j];$p(_RS,_RR,($_.i+1)%2)}}if($eq($_.format,"micro")){for(var _RW=8,_RV=$_.cols-1;_RW<=_RV;_RW+=1){$_.i=_RW;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=0;$_.qmv();var _Ra=$k[--$j];var _Rb=$k[--$j];$p(_Rb,_Ra,($_.i+1)%2);$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=$_.i;$_.qmv();var _Rf=$k[--$j];var _Rg=$k[--$j];$p(_Rg,_Rf,($_.i+1)%2)}}if($eq($_.format,"rmqr")){for(var _Rk=3,_Rj=$_.cols-4;_Rk<=_Rj;_Rk+=1){$_.i=_Rk;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=0;$_.qmv();var _Ro=$k[--$j];var _Rp=$k[--$j];$p(_Rp,_Ro,($_.i+1)%2);$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.rows-1;$_.qmv();var _Ru=$k[--$j];var _Rv=$k[--$j];$p(_Rv,_Ru,($_.i+1)%2)}for(var _Ry=3,_Rx=$_.rows-4;_Ry<=_Rx;_Ry+=1){$_.i=_Ry;$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=$_.i;$_.qmv();var _S2=$k[--$j];var _S3=$k[--$j];$p(_S3,_S2,($_.i+1)%2);$k[$j++]=$_.pixs;$k[$j++]=$_.cols-1;$k[$j++]=$_.i;$_.qmv();var _S8=$k[--$j];var _S9=$k[--$j];$p(_S9,_S8,($_.i+1)%2)}for(var _SF=$_.asp2-1,_SG=$f($_.asp3-$_.asp2),_SE=$_.cols-13;_SG<0?_SF>=_SE:_SF<=_SE;_SF+=_SG){$_.i=_SF;for(var _SJ=3,_SI=$_.rows-4;_SJ<=_SI;_SJ+=1){$_.j=_SJ;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.j;$_.qmv();var _SO=$k[--$j];var _SP=$k[--$j];$p(_SP,_SO,($_.j+1)%2)}}}$_.fpats=$g($_.qrcode_fpatmap,$_.format);for(var _SS=0;_SS<=7;_SS+=1){$_.y=_SS;for(var _ST=0;_ST<=7;_ST+=1){$_.x=_ST;$_.fpb0=$g($g($g($_.fpats,0),$_.y),$_.x);$_.fpb1=$g($g($g($_.fpats,1),$_.y),$_.x);$_.fpb2=$g($g($g($_.fpats,2),$_.y),$_.x);$_.fpb3=$g($g($g($_.fpats,3),$_.y),$_.x);if($_.fpb0!=9&&$_.y<$_.rows){$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.qmv();var _Sz=$k[--$j];var _T0=$k[--$j];$p(_T0,_Sz,$_.fpb0)}if($_.fpb1!=9){$k[$j++]=$_.pixs;$k[$j++]=$f($_.cols-$_.x)-1;$k[$j++]=$_.y;$_.qmv();var _T7=$k[--$j];var _T8=$k[--$j];$p(_T8,_T7,$_.fpb1)}if($_.fpb2!=9){$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$f($_.rows-$_.y)-1;$_.qmv();var _TF=$k[--$j];var _TG=$k[--$j];$p(_TG,_TF,$_.fpb2)}if($_.fpb3!=9){$k[$j++]=$_.pixs;$k[$j++]=$f($_.cols-$_.x)-1;$k[$j++]=$f($_.rows-$_.y)-1;$_.qmv();var _TO=$k[--$j];var _TP=$k[--$j];$p(_TP,_TO,$_.fpb3)}}}$_.putalgnpat=function(){var _TQ=$k[--$j];$_.py=_TQ;var _TR=$k[--$j];$_.px=_TR;for(var _TS=0;_TS<=4;_TS+=1){$_.pb=_TS;for(var _TT=0;_TT<=4;_TT+=1){$_.pa=_TT;$_.algnb=$g($g($_.algnpat,$_.pb),$_.pa);if($_.algnb!=9){$k[$j++]=$_.pixs;$k[$j++]=$f($_.px+$_.pa);$k[$j++]=$f($_.py+$_.pb);$_.qmv();var _Tg=$k[--$j];var _Th=$k[--$j];$p(_Th,_Tg,$_.algnb)}}}};if($eq($_.format,"full")){$_.algnpat=$_.qrcode_algnpatfull;for(var _To=$_.asp2-2,_Tp=$f($_.asp3-$_.asp2),_Tn=$_.cols-13;_Tp<0?_To>=_Tn:_To<=_Tn;_To+=_Tp){$_.i=_To;$k[$j++]=$_.i;$k[$j++]=4;$_.putalgnpat();$k[$j++]=4;$k[$j++]=$_.i;$_.putalgnpat()}for(var _Tx=$_.asp2-2,_Ty=$f($_.asp3-$_.asp2),_Tw=$_.cols-9;_Ty<0?_Tx>=_Tw:_Tx<=_Tw;_Tx+=_Ty){$_.x=_Tx;for(var _U4=$_.asp2-2,_U5=$f($_.asp3-$_.asp2),_U3=$_.rows-9;_U5<0?_U4>=_U3:_U4<=_U3;_U4+=_U5){$_.y=_U4;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.putalgnpat()}}}if($eq($_.format,"rmqr")){$_.algnpat=$_.qrcode_algnpatrmqr;for(var _UE=$_.asp2-2,_UF=$f($_.asp3-$_.asp2),_UD=$_.cols-13;_UF<0?_UE>=_UD:_UE<=_UD;_UE+=_UF){$_.i=_UE;$k[$j++]=$_.i;$k[$j++]=0;$_.putalgnpat();$k[$j++]=$_.i;$k[$j++]=$_.rows-3;$_.putalgnpat()}}$k[$j++]=Infinity;$F($g($_.qrcode_formatfimmap,$_.format),function(){var _UM=$k[--$j];$k[$j++]=Infinity;$F(_UM,function(){var _UO=$k[--$j];$k[$j++]=$_.rows;$k[$j++]=$_.cols;if(_UO()===true){return true}$as($a(2))});var _UR=$a();$k[$j++]=_UR});$_.formatmap=$a();var _US=$_.formatmap;for(var _UT=0,_UU=_US.length;_UT<_UU;_UT++){$F($g(_US,_UT),function(){var _UW=$k[--$j];$q(_UW);$_.qmv();var _UY=$k[--$j];$p($_.pixs,_UY,1)})}if($eq($_.format,"full")&&$_.cols>=45){$k[$j++]=Infinity;for(var _Ub=0,_Uc=$_.qrcode_vimmap.length;_Ub<_Uc;_Ub++){var _Uf=$g($_.qrcode_vimmap,_Ub);$k[$j++]=Infinity;$F(_Uf,function(){var _Uh=$k[--$j];$k[$j++]=$_.cols;if(_Uh()===true){return true}$as($a(2))});var _Uj=$a();$k[$j++]=_Uj}$_.versionmap=$a()}else{$_.versionmap=$a([])}var _Uk=$_.versionmap;for(var _Ul=0,_Um=_Uk.length;_Ul<_Um;_Ul++){$F($g(_Uk,_Ul),function(){var _Uo=$k[--$j];$q(_Uo);$_.qmv();var _Uq=$k[--$j];$p($_.pixs,_Uq,0)})}if($eq($_.format,"full")){$k[$j++]=$_.pixs;$k[$j++]=8;$k[$j++]=$_.rows-8;$_.qmv();var _Uu=$k[--$j];var _Uv=$k[--$j];$p(_Uv,_Uu,0)}if($_.mask==-1){$_.maskbits=$g($_.qrcode_formatmaskbits,$_.format)}else{$_.maskbits=$a([$g($g($_.qrcode_formatmaskbits,$_.format),$_.mask-1)]);$_.bestmaskval=$_.mask-1}$_.nmasks=$_.maskbits.length;$_.masklayers=$a($_.rows*$_.cols);for(var _VA=0,_V9=$_.rows-1;_VA<=_V9;_VA+=1){$_.j=_VA;for(var _VD=0,_VC=$_.cols-1;_VD<=_VC;_VD+=1){$_.i=_VD;$k[$j++]="pos";$k[$j++]=$_.i;$k[$j++]=$_.j;$_.qmv();var _VG=$k[--$j];var _VH=$k[--$j];$_[_VH]=_VG;var _VM=$g($_.pixs,$_.pos);$k[$j++]=$_.masklayers;$k[$j++]=$_.pos;$k[$j++]=_VM;if(_VM==-1){$k[$j-1]=$g($_.funcpatlayers,$_.j*6%72+$_.i%6)}else{var _VR=$k[--$j];$k[$j++]=_VR*255}var _VS=$k[--$j];var _VT=$k[--$j];var _VU=$k[--$j];$p(_VU,_VT,_VS)}}var _VX=$ne($_.format,"rmqr")?1:2;$_.posx=$f($_.cols-_VX);$_.posy=$_.rows-1;$_.dir=-1;$_.col=1;$_.num=0;for(;;){if($_.posx<0){break}$k[$j++]="pos";$k[$j++]=$_.posx;$k[$j++]=$_.posy;$_.qmv();var _Vc=$k[--$j];var _Vd=$k[--$j];$_[_Vd]=_Vc;if($g($_.pixs,$_.pos)==-1){$_.bit=$bs($g($_.cws,~~($_.num/8)),-(7-$_.num%8))&1;$p($_.masklayers,$_.pos,$g($_.masklayers,$_.pos)^$_.bit*255);$_.num=$_.num+1}if($_.col==1){$_.col=0;$_.posx=$_.posx-1}else{$_.col=1;$_.posx=$_.posx+1;$_.posy=$f($_.posy+$_.dir);if($_.posy<0||$_.posy>=$_.rows){$_.dir=$_.dir*-1;$_.posy=$f($_.posy+$_.dir);$_.posx=$_.posx-2;if($eq($_.format,"full")&&$_.posx==6){$_.posx=$_.posx-1}}}}$_.evalfulln1n3=function(){var _W7=$k[--$j];$_.scrle=_W7;$k[$j++]="scr1";$k[$j++]=0;$F($_.scrle,function(){var _W9=$k[$j-1];if(_W9>=5){var _WA=$k[--$j];var _WB=$k[--$j];var _WC=$f(_WB+_WA)-2;$k[$j++]=_WC;$k[$j++]=_WC}$j--});var _WD=$k[--$j];var _WE=$k[--$j];$_[_WE]=_WD;$_.scr3=0;for(var _WH=3,_WG=$_.scrle.length-3;_WH<=_WG;_WH+=2){$_.j=_WH;if($g($_.scrle,$_.j)%3==0){$_.fact=~~($g($_.scrle,$_.j)/3);var _WQ=$G($_.scrle,$_.j-2,5);for(var _WR=0,_WS=_WQ.length;_WR<_WS;_WR++){$k[$j++]=$g(_WQ,_WR)==$_.fact}var _WV=$k[--$j];var _WW=$k[--$j];$j--;var _WY=$an(_WW,_WV);var _WZ=$k[--$j];var _Wa=$k[--$j];if(_Wa&&(_WZ&&_WY)){if($_.j==3||$_.j+4>=$_.scrle.length){$_.scr3=$_.scr3+40}else{if($g($_.scrle,$_.j-3)>=4||$g($_.scrle,$_.j+3)>=4){$_.scr3=$_.scr3+40}}}}}$k[$j++]=$_.scr1;$k[$j++]=$_.scr3};$_.evalfull=function(){var _Wo=$k[--$j];$_.sym=_Wo;$_.n1=0;$_.n2=0;$_.n3=0;$_.rle=$a($_.cols+1);$_.lastpairs=$a($_.cols);$_.thispairs=$a($_.cols);$_.colsadd1=$_.cols+1;$_.earlyexit=false;for(var _Wy=0,_Wx=$_.cols-1;_Wy<=_Wx;_Wy+=1){$_.i=_Wy;$k[$j++]=Infinity;var _X0=$_.cols;$k[$j++]=0;$k[$j++]=0;for(var _X2=$_.i,_X3=_X0,_X1=_X0*_X0-1;_X3<0?_X2>=_X1:_X2<=_X1;_X2+=_X3){var _X5=$g($_.sym,_X2);var _X6=$k[--$j];$k[$j++]=_X5;if($eq(_X6,_X5)){var _X7=$k[--$j];$k[$j-1]+=1;$k[$j++]=_X7}else{var _X9=$k[--$j];$k[$j++]=1;$k[$j++]=_X9}}$j--;var _XB=$m()+2;$as($G($_.rle,0,_XB-2));$_.evalfulln1n3();var _XE=$k[--$j];$_.n3=$f(_XE+$_.n3);var _XG=$k[--$j];$_.n1=$f(_XG+$_.n1);$j--;$_.symrow=$G($_.sym,$_.i*$_.cols,$_.cols);$k[$j++]=Infinity;var _XM=$_.symrow;$k[$j++]=0;$k[$j++]=0;for(var _XN=0,_XO=_XM.length;_XN<_XO;_XN++){var _XP=$g(_XM,_XN);var _XQ=$k[--$j];$k[$j++]=_XP;if($eq(_XQ,_XP)){var _XR=$k[--$j];$k[$j-1]+=1;$k[$j++]=_XR}else{var _XT=$k[--$j];$k[$j++]=1;$k[$j++]=_XT}}$j--;var _XV=$m()+2;$as($G($_.rle,0,_XV-2));$_.evalfulln1n3();var _XY=$k[--$j];$_.n3=$f(_XY+$_.n3);var _Xa=$k[--$j];$_.n1=$f(_Xa+$_.n1);$j--;var _Xb=$_.thispairs;$_.thispairs=$_.lastpairs;$_.lastpairs=_Xb;var _Xf=$g($_.symrow,0)==1?0:1;var _Xg=$_.symrow;$k[$j++]=_Xf;for(var _Xh=0,_Xi=_Xg.length;_Xh<_Xi;_Xh++){var _Xj=$g(_Xg,_Xh);var _Xk=$k[--$j];$k[$j++]=$f(_Xk+_Xj);$k[$j++]=_Xj}$j--;$as($_.thispairs);$j--;if($_.i>0){$k[$j++]=Infinity;$q($_.lastpairs);$q($_.thispairs);$k[$j++]=$_.n2;for(var _Xr=0,_Xs=$_.cols;_Xr<_Xs;_Xr++){var _Xt=$k[--$j];var _Xu=$k[--$j];$k[$j++]=_Xt;$k[$j++]=_Xu;var _Xw=$k[$j-1-$_.colsadd1];var _Xx=$k[--$j];if(($f(_Xx+_Xw)&3)==0){$k[$j-1]+=3}}var _Xz=$k[--$j];$_.n2=_Xz;$l()}if($f($_.n1+$_.n2+$_.n3)>=$_.bestscore){$_.earlyexit=true;break}}if(!$_.earlyexit){$k[$j++]="dark";var _Y8=0;$F($_.sym,function(){var _Y6=$k[--$j];_Y8=$f(_Y8+_Y6)});var _Y9=$k[--$j];$_[_Y9]=_Y8;var _YB=$_.cols;$_.n4=~~($abs($_.dark*100/(_YB*_YB)-50)/5)*10;$k[$j++]=$f($_.n1+$_.n2+$_.n3+$_.n4)}else{$k[$j++]=999999999}};$_.evalmicro=function(){var _YG=$k[--$j];$_.sym=_YG;$_.dkrhs=0;$_.dkbot=0;for(var _YJ=1,_YI=$_.cols-1;_YJ<=_YI;_YJ+=1){$_.i=_YJ;$k[$j++]="dkrhs";$k[$j++]=$_.dkrhs;$k[$j++]=$_.sym;$k[$j++]=$_.cols-1;$k[$j++]=$_.i;$_.qmv();var _YO=$k[--$j];var _YP=$k[--$j];var _YR=$k[--$j];var _YS=$k[--$j];$_[_YS]=$f(_YR+$g(_YP,_YO));$k[$j++]="dkbot";$k[$j++]=$_.dkbot;$k[$j++]=$_.sym;$k[$j++]=$_.i;$k[$j++]=$_.cols-1;$_.qmv();var _YX=$k[--$j];var _YY=$k[--$j];var _Ya=$k[--$j];var _Yb=$k[--$j];$_[_Yb]=$f(_Ya+$g(_YY,_YX))}if($_.dkrhs<=$_.dkbot){$k[$j++]=-($_.dkrhs*16+$_.dkbot)}else{$k[$j++]=-($_.dkbot*16+$_.dkrhs)}};$_.bestscore=999999999;for(var _Yk=0,_Yj=$_.nmasks-1;_Yk<=_Yj;_Yk+=1){$_.m=_Yk;$_.maskbit=$g($_.maskbits,$_.m);$_.masksym=$a($_.rows*$_.cols);for(var _Yu=0,_Yt=$_.rows*$_.cols-1;_Yu<=_Yt;_Yu+=1){$_.i=_Yu;$p($_.masksym,$_.i,$bs($g($_.masklayers,$_.i),-$_.maskbit)&1)}if($_.nmasks!=1){$k[$j++]="score";$k[$j++]=$_.masksym;if($eq($_.format,"full")){$_.evalfull()}else{$_.evalmicro()}var _Z4=$k[--$j];var _Z5=$k[--$j];$_[_Z5]=_Z4;if($_.score<$_.bestscore){$_.bestsym=$_.masksym;$_.bestmaskval=$_.m;$_.bestscore=$_.score}}else{$_.bestsym=$_.masksym}}$_.pixs=$_.bestsym;if($eq($_.format,"full")){$k[$j++]=$_.pixs;$k[$j++]=8;$k[$j++]=$_.cols-8;$_.qmv();var _ZG=$k[--$j];var _ZH=$k[--$j];$p(_ZH,_ZG,1)}if($eq($_.format,"full")){$_.ecid=$g($_.qrcode_ecidfull,$_.eclval);$_.fmtval=$g($_.fmtvalsfull,$_.ecid*8+$_.bestmaskval);for(var _ZR=0,_ZQ=$_.formatmap.length-1;_ZR<=_ZQ;_ZR+=1){$_.i=_ZR;$F($g($_.formatmap,$_.i),function(){var _ZW=$k[--$j];$k[$j++]=$_.pixs;$q(_ZW);$_.qmv();var _ZZ=$k[--$j];var _Za=$k[--$j];$p(_Za,_ZZ,$bs($_.fmtval,-(14-$_.i))&1)})}}if($eq($_.format,"micro")){$_.symid=$g($g($_.qrcode_ecidmicrosym,~~(($_.cols-11)/2)),$_.eclval);$_.fmtval=$g($_.fmtvalsmicro,$_.symid*4+$_.bestmaskval);for(var _Zm=0,_Zl=$_.formatmap.length-1;_Zm<=_Zl;_Zm+=1){$_.i=_Zm;var _Zr=$g($g($_.formatmap,$_.i),0);$k[$j++]=$_.pixs;$q(_Zr);$_.qmv();var _Zu=$k[--$j];var _Zv=$k[--$j];$p(_Zv,_Zu,$bs($_.fmtval,-(14-$_.i))&1)}}if($eq($_.format,"rmqr")){$_.fmtvalu=$g($_.qrcode_ecidrmqr,$_.eclval)*32+$_.verind;$_.fmtval1=$g($_.fmtvalsrmqr1,$_.fmtvalu);$_.fmtval2=$g($_.fmtvalsrmqr2,$_.fmtvalu);for(var _a8=0,_a7=$_.formatmap.length-1;_a8<=_a7;_a8+=1){$_.i=_a8;var _aD=$g($g($_.formatmap,$_.i),0);$k[$j++]=$_.pixs;$q(_aD);$_.qmv();var _aG=$k[--$j];var _aH=$k[--$j];$p(_aH,_aG,$bs($_.fmtval1,-(17-$_.i))&1);var _aM=$g($g($_.formatmap,$_.i),1);$k[$j++]=$_.pixs;$q(_aM);$_.qmv();var _aP=$k[--$j];var _aQ=$k[--$j];$p(_aQ,_aP,$bs($_.fmtval2,-(17-$_.i))&1)}}if($eq($_.format,"full")&&$_.cols>=45){$_.verval=$g($_.vervals,~~(($_.cols-17)/4)-7);for(var _aY=0,_aX=$_.versionmap.length-1;_aY<=_aX;_aY+=1){$_.i=_aY;$F($g($_.versionmap,$_.i),function(){var _ad=$k[--$j];$k[$j++]=$_.pixs;$q(_ad);$_.qmv();var _ag=$k[--$j];var _ah=$k[--$j];$p(_ah,_ag,$bs($_.verval,-(17-$_.i))&1)})}}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renmatrix";$k[$j++]="pixs";$k[$j++]=$_.pixs;$k[$j++]="pixx";$k[$j++]=$_.cols;$k[$j++]="pixy";$k[$j++]=$_.rows;$k[$j++]="height";$k[$j++]=$_.rows*$_.modunit/72;$k[$j++]="width";$k[$j++]=$_.cols*$_.modunit/72;$k[$j++]="borderleft";if($eq($_.format,"full")){$k[$j++]=4}else{$k[$j++]=2}$k[$j++]="borderright";if($eq($_.format,"full")){$k[$j++]=4}else{$k[$j++]=2}$k[$j++]="bordertop";if($eq($_.format,"full")){$k[$j++]=4}else{$k[$j++]=2}$k[$j++]="borderbottom";if($eq($_.format,"full")){$k[$j++]=4}else{$k[$j++]=2}$k[$j++]="opt";$k[$j++]=$_.options;var _au=$d();$k[$j++]=_au;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_swissqrcode(){var $__=$_;$_=Object.assign({},$_,bwipp_swissqrcode.globals);$_.dontdraw=false;$_.parse=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _7=$k[--$j];$_.barcode=_7;$_.barlen=$_.barcode.length;$del($_.options,"parse");if($_.barcode.length>997){$k[$j++]="bwipp.swissqrcodeBadLength#28091";$k[$j++]="Swiss QR Code input must not exceed 997 digits";bwipp_raiseerror()}$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _D=$k[--$j];$_.args=_D;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){$$.save();var _I=$$.currpos();$$.translate(_I.x,_I.y);var _J=72/25.4;$$.scale(_J,_J);$$.save();$$.newpath();$$.moveto(0,0);$$.lineto(46,0);$$.lineto(46,46);$$.lineto(0,46);$$.closepath();$$.moveto(19.5,19.5);$$.lineto(19.5,26.5);$$.lineto(26.5,26.5);$$.lineto(26.5,19.5);$$.closepath();$$.clip();$$.save();$$.moveto(0,0);var _M=46/$g($_.args,"pixx")/2;$$.scale(_M,_M);bwipp_renmatrix();$$.restore();$$.restore();$$.translate(19.5,19.5);var _N=7/83;$$.scale(_N,_N);$$.newpath();$$.moveto(6,6);$$.lineto(6,77);$$.lineto(77,77);$$.lineto(77,6);$$.closepath();$$.moveto(49,18);$$.lineto(49,34);$$.lineto(65,34);$$.lineto(65,49);$$.lineto(49,49);$$.lineto(49,65);$$.lineto(34,65);$$.lineto(34,49);$$.lineto(18,49);$$.lineto(18,34);$$.lineto(34,34);$$.lineto(34,18);$$.closepath();$$.setrgbcolor(0,0,0);$$.fill();$$.restore()}$_=$__}function bwipp_microqrcode(){var $__=$_;$_=Object.assign({},$_,bwipp_microqrcode.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","micro");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_rectangularmicroqrcode(){var $__=$_;$_=Object.assign({},$_,bwipp_rectangularmicroqrcode.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","rmqr");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_maxicode(){if(!bwipp_maxicode.globals){var $__=$_;$_=bwipp_maxicode.globals={};$_.maxicode_eci=-1;$_.maxicode_pad=-2;$_.maxicode_ns=-3;$_.maxicode_la=-4;$_.maxicode_lb=-5;$_.maxicode_sa=-6;$_.maxicode_sb=-7;$_.maxicode_sc=-8;$_.maxicode_sd=-9;$_.maxicode_se=-10;$_.maxicode_sa2=-11;$_.maxicode_sa3=-12;$_.maxicode_lkc=-13;$_.maxicode_lkd=-14;$_.maxicode_lke=-15;$_.maxicode_pd2=-16;$_.maxicode_pd3=-17;$_.eci=$_.maxicode_eci;$_.pad=$_.maxicode_pad;$_.ns=$_.maxicode_ns;$_.la=$_.maxicode_la;$_.lb=$_.maxicode_lb;$_.sa=$_.maxicode_sa;$_.sb=$_.maxicode_sb;$_.sc=$_.maxicode_sc;$_.sd=$_.maxicode_sd;$_.se=$_.maxicode_se;$_.sa2=$_.maxicode_sa2;$_.sa3=$_.maxicode_sa3;$_.lkc=$_.maxicode_lkc;$_.lkd=$_.maxicode_lkd;$_.lke=$_.maxicode_lke;$_.pd2=$_.maxicode_pd2;$_.pd3=$_.maxicode_pd3;$_.maxicode_charmaps=$a([$a([13,"`",192,224,0]),$a(["A","a",193,225,1]),$a(["B","b",194,226,2]),$a(["C","c",195,227,3]),$a(["D","d",196,228,4]),$a(["E","e",197,229,5]),$a(["F","f",198,230,6]),$a(["G","g",199,231,7]),$a(["H","h",200,232,8]),$a(["I","i",201,233,9]),$a(["J","j",202,234,10]),$a(["K","k",203,235,11]),$a(["L","l",204,236,12]),$a(["M","m",205,237,13]),$a(["N","n",206,238,14]),$a(["O","o",207,239,15]),$a(["P","p",208,240,16]),$a(["Q","q",209,241,17]),$a(["R","r",210,242,18]),$a(["S","s",211,243,19]),$a(["T","t",212,244,20]),$a(["U","u",213,245,21]),$a(["V","v",214,246,22]),$a(["W","w",215,247,23]),$a(["X","x",216,248,24]),$a(["Y","y",217,249,25]),$a(["Z","z",218,250,26]),$a([$_.eci,$_.eci,$_.eci,$_.eci,$_.eci]),$a([28,28,28,28,$_.pad]),$a([29,29,29,29,$_.pd2]),$a([30,30,30,30,27]),$a([$_.ns,$_.ns,$_.ns,$_.ns,$_.ns]),$a([" ","{",219,251,28]),$a([$_.pad,$_.pad,220,252,29]),$a(['"',"}",221,253,30]),$a(["#","~",222,254,31]),$a(["$",127,223,255,159]),$a(["%",";",170,161,160]),$a(["&","<",172,168,162]),$a(["'","=",177,171,163]),$a([40,">",178,175,164]),$a([41,"?",179,176,165]),$a(["*","[",181,180,166]),$a(["+",92,185,183,167]),$a([",","]",186,184,169]),$a(["-","^",188,187,173]),$a([".","_",189,191,174]),$a(["/"," ",190,138,182]),$a(["0",",",128,139,149]),$a(["1",".",129,140,150]),$a(["2","/",130,141,151]),$a(["3",":",131,142,152]),$a(["4","@",132,143,153]),$a(["5","!",133,144,154]),$a(["6","|",134,145,155]),$a(["7",$_.pd2,135,146,156]),$a(["8",$_.sa2,136,147,157]),$a(["9",$_.sa3,137,148,158]),$a([":",$_.pd3,$_.la,$_.la,$_.la]),$a([$_.sb,$_.sa," "," "," "]),$a([$_.sc,$_.sc,$_.lkc,$_.sc,$_.sc]),$a([$_.sd,$_.sd,$_.sd,$_.lkd,$_.sd]),$a([$_.se,$_.se,$_.se,$_.se,$_.lke]),$a([$_.lb,$_.la,$_.lb,$_.lb,$_.lb])]);$_.maxicode_modmap=$a([469,529,286,316,347,346,673,672,703,702,647,676,283,282,313,312,370,610,618,379,378,409,408,439,705,704,559,589,588,619,458,518,640,701,675,674,285,284,315,314,310,340,531,289,288,319,349,348,456,486,517,516,471,470,369,368,399,398,429,428,549,548,579,578,609,608,649,648,679,678,709,708,639,638,669,668,699,698,279,278,309,308,339,338,381,380,411,410,441,440,561,560,591,590,621,620,547,546,577,576,607,606,367,366,397,396,427,426,291,290,321,320,351,350,651,650,681,680,711,710,1,0,31,30,61,60,3,2,33,32,63,62,5,4,35,34,65,64,7,6,37,36,67,66,9,8,39,38,69,68,11,10,41,40,71,70,13,12,43,42,73,72,15,14,45,44,75,74,17,16,47,46,77,76,19,18,49,48,79,78,21,20,51,50,81,80,23,22,53,52,83,82,25,24,55,54,85,84,27,26,57,56,87,86,117,116,147,146,177,176,115,114,145,144,175,174,113,112,143,142,173,172,111,110,141,140,171,170,109,108,139,138,169,168,107,106,137,136,167,166,105,104,135,134,165,164,103,102,133,132,163,162,101,100,131,130,161,160,99,98,129,128,159,158,97,96,127,126,157,156,95,94,125,124,155,154,93,92,123,122,153,152,91,90,121,120,151,150,181,180,211,210,241,240,183,182,213,212,243,242,185,184,215,214,245,244,187,186,217,216,247,246,189,188,219,218,249,248,191,190,221,220,251,250,193,192,223,222,253,252,195,194,225,224,255,254,197,196,227,226,257,256,199,198,229,228,259,258,201,200,231,230,261,260,203,202,233,232,263,262,205,204,235,234,265,264,207,206,237,236,267,266,297,296,327,326,357,356,295,294,325,324,355,354,293,292,323,322,353,352,277,276,307,306,337,336,275,274,305,304,335,334,273,272,303,302,333,332,271,270,301,300,331,330,361,360,391,390,421,420,363,362,393,392,423,422,365,364,395,394,425,424,383,382,413,412,443,442,385,384,415,414,445,444,387,386,417,416,447,446,477,476,507,506,537,536,475,474,505,504,535,534,473,472,503,502,533,532,455,454,485,484,515,514,453,452,483,482,513,512,451,450,481,480,511,510,541,540,571,570,601,600,543,542,573,572,603,602,545,544,575,574,605,604,563,562,593,592,623,622,565,564,595,594,625,624,567,566,597,596,627,626,657,656,687,686,717,716,655,654,685,684,715,714,653,652,683,682,713,712,637,636,667,666,697,696,635,634,665,664,695,694,633,632,663,662,693,692,631,630,661,660,691,690,721,720,751,750,781,780,723,722,753,752,783,782,725,724,755,754,785,784,727,726,757,756,787,786,729,728,759,758,789,788,731,730,761,760,791,790,733,732,763,762,793,792,735,734,765,764,795,794,737,736,767,766,797,796,739,738,769,768,799,798,741,740,771,770,801,800,743,742,773,772,803,802,745,744,775,774,805,804,747,746,777,776,807,806,837,836,867,866,897,896,835,834,865,864,895,894,833,832,863,862,893,892,831,830,861,860,891,890,829,828,859,858,889,888,827,826,857,856,887,886,825,824,855,854,885,884,823,822,853,852,883,882,821,820,851,850,881,880,819,818,849,848,879,878,817,816,847,846,877,876,815,814,845,844,875,874,813,812,843,842,873,872,811,810,841,840,871,870,901,900,931,930,961,960,903,902,933,932,963,962,905,904,935,934,965,964,907,906,937,936,967,966,909,908,939,938,969,968,911,910,941,940,971,970,913,912,943,942,973,972,915,914,945,944,975,974,917,916,947,946,977,976,919,918,949,948,979,978,921,920,951,950,981,980,923,922,953,952,983,982,925,924,955,954,985,984,927,926,957,956,987,986,58,89,88,118,149,148,178,209,208,238,269,268,298,329,328,358,389,388,418,449,448,478,509,508,538,569,568,598,629,628,658,689,688,718,749,748,778,809,808,838,869,868,898,929,928,958,989,988]);$_.maxicode_latchseq=$a([$a([$a([]),$a([63]),$a([58]),$a([58]),$a([58])]),$a([$a([63]),$a([]),$a([63]),$a([63]),$a([63])]),$a([$a([60,60]),$a([60,60]),$a([]),$a([60,60]),$a([60,60])]),$a([$a([61,61]),$a([61,61]),$a([61,61]),$a([]),$a([61,61])]),$a([$a([62,62]),$a([62,62]),$a([62,62]),$a([62,62]),$a([])])]);$_.maxicode_latchlen=$a([$a([0,1,1,1,1]),$a([1,0,1,1,1]),$a([2,2,0,2,2]),$a([2,2,2,0,2]),$a([2,2,2,2,0])]);$_.maxicode_pad_code=$a([33,33,0,0,28]);$_.maxicode_max_int=2147483647;$_.maxicode_rsprod=function(){var _2N=$k[$j-1];var _2O=$k[$j-2];if(_2N!=0&&_2O!=0){var _2Q=$k[--$j];var _2S=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_2Q)+$g($_.rslog,_2S))%63)}else{$j-=2;$k[$j++]=0}};$_.maxicode_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _2Y=0,_2Z=$_.k;_2Y<_2Z;_2Y++){$k[$j++]=0}$_.coeffs=$a();for(var _2c=1,_2b=$_.k;_2c<=_2b;_2c+=1){var _2d=$_.coeffs;$p(_2d,_2c,$g(_2d,_2c-1));$k[$j++]=$g($_.rsalog,_2c);for(var _2h=_2c-1;_2h>=1;_2h-=1){var _2i=$_.coeffs;var _2k=$k[$j-1];$k[$j++]=_2i;$k[$j++]=_2h;$k[$j++]=$g(_2i,_2h);$k[$j++]=_2k;$_.maxicode_rsprod();var _2m=$k[--$j];var _2n=$k[--$j];var _2p=$k[--$j];$p(_2p,_2n,$xo(_2m,$g($_.coeffs,_2n-1)))}var _2q=$_.coeffs;var _2s=$k[$j-1];$k[$j++]=_2q;$k[$j++]=0;$k[$j++]=$g(_2q,0);$k[$j++]=_2s;$_.maxicode_rsprod();var _2t=$k[--$j];var _2u=$k[--$j];var _2v=$k[--$j];$p(_2v,_2u,_2t);$j--}$k[$j++]=$G($_.coeffs,0,$_.k)};$_.maxicode_coeffscachemax=3;$_.maxicode_coeffscachelimit=58;$k[$j++]=$_.maxicode_coeffscachemax;$k[$j++]=$_.maxicode_coeffscachelimit;bwipp_fifocache();var _31=$k[--$j];$_.maxicode_coeffscache=_31;$_.maxicode_coeffscachefetch=function(){var _32=$k[--$j];$_.k=_32;var _34=$g($_.maxicode_coeffscache,"fetch");$k[$j++]=$_.k;$k[$j++]=function(){$_.maxicode_gencoeffs()};$k[$j++]=function(){var _35=$k[--$j];$k[$j++]=_35.length};if(_34()===true){return true}};$_.charvals=$a([new Map,new Map,new Map,new Map,new Map]);for(var _37=0,_36=$_.maxicode_charmaps.length-1;_37<=_36;_37+=1){$_.i=_37;$_.encs=$g($_.maxicode_charmaps,$_.i);for(var _3A=0;_3A<=4;_3A+=1){$_.j=_3A;var _3D=$g($_.encs,$_.j);var _3E=$t(_3D);if($eq(_3E,"stringtype")){_3D=$g(_3D,0)}$p($g($_.charvals,$_.j),_3D,$_.i)}}$_.seta=$g($_.charvals,0);$_.setb=$g($_.charvals,1);$_.setc=$g($_.charvals,2);$_.setd=$g($_.charvals,3);$_.sete=$g($_.charvals,4);$_.charvals=$_.charvals;$k[$j++]=Infinity;$k[$j++]=1;for(var _3X=0;_3X<63;_3X++){var _3Y=$k[$j-1];var _3Z=_3Y*2;$k[$j++]=_3Z;if(_3Z>=64){var _3a=$k[--$j];$k[$j++]=_3a^67}}var _3b=$a();$_.rsalog=_3b;$_.rslog=$a(64);for(var _3d=1;_3d<=63;_3d+=1){$p($_.rslog,$g($_.rsalog,_3d),_3d)}$_.rslog=$_.rslog;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_maxicode.globals);$_.dontdraw=false;$_.modunit=2;$_.mode=-1;$_.sam=-1;$_.parse=false;$_.parsefnc=false;$_.legacyencoder=false;bwipp_processoptions();var _3i=$k[--$j];$_.options=_3i;var _3j=$k[--$j];$_.barcode=_3j;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.maxicodeEmptyData#28653";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>1e3){$k[$j++]="bwipp.maxicodeinputTooLong#28658";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.mode!=-1&&($_.mode<2||$_.mode>6)){$k[$j++]="bwipp.maxicodeBadMode#28662";$k[$j++]="Mode must be 2 to 6";bwipp_raiseerror()}if($_.sam!=-1){$k[$j++]=true;for(var _3s=0;_3s<1;_3s++){var _3t=$_.sam;if(_3t<12||_3t>88){$k[$j-1]=false;break}var _3v=$_.sam%10;if(_3v<2||_3v>8){$k[$j-1]=false;break}if(~~($_.sam/10)>$_.sam%10){$k[$j-1]=false;break}}var _3y=$k[--$j];if(!_3y){$k[$j++]="bwipp.maxicodeBadSAM#28673";$k[$j++]='SAM must be formatted as "NM" for Nth of M symbols, from 2 to 8 symbols';bwipp_raiseerror()}}$_.encoding="new";if($_.legacyencoder){$_.encoding="legacy"}$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _44=$k[--$j];$_.msg=_44;$_.msglen=$_.msg.length;if($_.mode==2||$_.mode==3){$_.barcode=$s($_.msglen);$_.barlen=$_.msglen;for(var _4D=0,_4C=$_.msglen-1;_4D<=_4C;_4D+=1){$_.i=_4D;if($g($_.msg,$_.i)<0){$_.barlen=$_.i;break}$p($_.barcode,$_.i,$g($_.msg,$_.i))}$_.difflen=$_.msglen-$_.barlen;$_.fid="";if($_.barlen>=9){var _4Q=$_.barcode;var _4S=$g(_4Q,7);var _4T=$g(_4Q,8);if($eq($G(_4Q,0,7),"[)>01")&&(_4S>=48&&_4S<=57&&(_4T>=48&&_4T<=57))){$_.fid=$G($_.barcode,0,9);$_.barcode=$G($_.barcode,9,$_.barlen-9)}}$X($_.barcode,"");var _4a=$k[--$j];if(_4a){var _4b=$k[--$j];$_.pcode=_4b;$j--;if($_.mode==2){$k[$j++]=true;for(var _4d=0;_4d<1;_4d++){var _4f=$_.pcode.length;if(_4f>9||_4f==0){$k[$j-1]=false;break}$F($_.pcode,function(){var _4h=$k[--$j];if(_4h<48||_4h>57){$k[$j-1]=false;return true}})}var _4i=$k[--$j];if(!_4i){$k[$j-1]="bwipp.maxicodeBadMode2PostCode#28739";$k[$j++]="A mode 2 postcode must not be empty or exceed 9 digits";bwipp_raiseerror()}}else{$k[$j++]=true;for(var _4j=0;_4j<1;_4j++){var _4l=$_.pcode.length;if(_4l>6||_4l==0){$k[$j-1]=false;break}$F($_.pcode,function(){var _4n=$k[--$j];if(!(_4n==32||(_4n>=34&&_4n<=58||_4n>=65&&_4n<=90))){$k[$j-1]=false;return true}})}var _4o=$k[--$j];if(!_4o){$k[$j-1]="bwipp.maxicodeBadMode3PostCode#28753";$k[$j++]="A mode 3 postcode must not be empty or exceed 6 characters";bwipp_raiseerror()}}}else{$k[$j-1]="bwipp.maxicodeExpectedPostCode#28757";$k[$j++]="Expected postcode followed by group separator character";bwipp_raiseerror()}var _4p=$k[--$j];$X(_4p,"");var _4q=$k[--$j];if(_4q){var _4r=$k[--$j];$_.ccode=_4r;$k[$j-1]=true;for(var _4s=0;_4s<1;_4s++){if($_.ccode.length!=3){$k[$j-1]=false;break}$F($_.ccode,function(){var _4v=$k[--$j];if(_4v<48||_4v>57){$k[$j-1]=false;return true}})}var _4w=$k[--$j];if(!_4w){$k[$j-1]="bwipp.maxicodeBadCountryCode#28767";$k[$j++]="Country code must be three digits";bwipp_raiseerror()}}else{$k[$j-1]="bwipp.maxicodeExpectedCountryCode#28770";$k[$j++]="Expected country code followed by group separator character";bwipp_raiseerror()}var _4x=$k[--$j];$X(_4x,"");var _4y=$k[--$j];if(_4y){var _4z=$k[--$j];$_.scode=_4z;$k[$j-1]=true;for(var _50=0;_50<1;_50++){if($_.scode.length!=3){$k[$j-1]=false;break}$F($_.scode,function(){var _53=$k[--$j];if(_53<48||_53>57){$k[$j-1]=false;return true}})}var _54=$k[--$j];if(!_54){$k[$j-1]="bwipp.maxicodeBadServiceClass#28780";$k[$j++]="Service class must be three digits";bwipp_raiseerror()}}else{$k[$j-1]="bwipp.maxicodeExpectedServiceClass#28783";$k[$j++]="Expected service class followed by group separator character";bwipp_raiseerror()}var _55=$k[--$j];$_.barcode=_55;var _58=$s($_.barcode.length+$_.fid.length);$P(_58,0,$_.fid);$P(_58,$_.fid.length,$_.barcode);$_.barcode=_58;$_.barlen=$_.barcode.length;$k[$j++]=Infinity;var _5D=$_.barcode;for(var _5E=0,_5F=_5D.length;_5E<_5F;_5E++){$k[$j++]=$g(_5D,_5E)}$q($G($_.msg,$_.msglen-$_.difflen,$_.difflen));$_.msg=$a();$_.msglen=$_.msg.length}var _5Q=$_.mode==5?77:$_.mode<=3?84:93;$_.maxlen=_5Q;if($eq($_.encoding,"legacy")){$k[$j++]=Infinity;for(var _5U=0,_5V=$_.msglen+1;_5U<_5V;_5U++){$k[$j++]=0}$_.nseq=$a();for(var _5X=$_.msglen-1;_5X>=0;_5X-=1){$_.i=_5X;var _5a=$g($_.msg,$_.i);if(_5a>=48&&_5a<=57){$p($_.nseq,$_.i,$g($_.nseq,$_.i+1)+1)}else{$p($_.nseq,$_.i,0)}}$_.nseq=$G($_.nseq,0,$_.msglen);$_.prefixinset=function(){$k[$j++]=0;for(;;){var _5l=$k[$j-1];var _5m=$k[$j-2];if(_5l>=_5m.length){break}var _5n=$k[$j-1];var _5o=$k[$j-2];var _5q=$k[$j-3];var _5r=$has(_5q,$g(_5o,_5n));if(_5r){$k[$j-1]+=1}else{break}}var _5t=$k[--$j];$j--;$k[$j-1]=_5t};$_.enc=function(){var _5x=$k[--$j];var _5y=$k[--$j];$p($_.out,$_.j,$g(_5x,_5y));$_.j=$_.j+1};$_.out=$a(144);$_.i=0;$_.j=0;$_.cset="seta";for(;;){if($_.i==$_.msglen){if($ne($_.cset,"seta")&&$ne($_.cset,"setb")&&$_.out.length<$_.maxlen){var _6B=$_[$_.cset];$k[$j++]=$_.maxicode_la;$k[$j++]=_6B;$_.enc();$_.cset="seta"}break}for(;;){if($g($_.msg,$_.i)<=-1e6){var _6G=$_[$_.cset];$k[$j++]=$_.maxicode_eci;$k[$j++]=_6G;$_.enc();var _6K=-$g($_.msg,$_.i)-1e6;$k[$j++]=_6K;if(_6K<=31){var _6L=$k[--$j];var _6M=$a(1);$k[$j++]=_6L&63;$as(_6M)}else{var _6N=$k[$j-1];if(_6N<=1023){var _6O=$k[--$j];var _6P=$a(2);$k[$j++]=$flr(_6O/64)&31|32;$k[$j++]=_6O&63;$as(_6P)}else{var _6Q=$k[$j-1];if(_6Q<=32767){var _6R=$k[--$j];var _6S=$a(3);$k[$j++]=$flr(_6R/4096)&47|48;$k[$j++]=$flr(_6R/64)&63;$k[$j++]=_6R&63;$as(_6S)}else{var _6T=$k[--$j];var _6U=$a(4);$k[$j++]=$flr(_6T/262144)&55|56;$k[$j++]=$flr(_6T/4096)&63;$k[$j++]=$flr(_6T/64)&63;$k[$j++]=_6T&63;$as(_6U)}}}var _6V=$k[--$j];$P($_.out,$_.j,_6V);$_.j=_6V.length+$_.j;$_.i=$_.i+1;break}if($g($_.nseq,$_.i)>=9){var _6f=$G($_.msg,$_.i,9);var _6k=0;for(var _6g=0,_6h=_6f.length;_6g<_6h;_6g++){_6k=$f(_6k+($g(_6f,_6g)-48))*10}$k[$j++]=~~(_6k/10);for(var _6l=0;_6l<4;_6l++){var _6m=$k[--$j];$k[$j++]=_6m&63;$k[$j++]=$flr(_6m/64)}var _6o=$_[$_.cset];$k[$j++]=$g(_6o,$_.maxicode_ns);for(var _6q=0;_6q<=10;_6q+=2){var _6r=$k[$j-1-_6q];$k[$j++]=_6r}$as($a(6));$r(7,1);for(var _6t=0;_6t<6;_6t++){$j--}var _6v=$k[--$j];$P($_.out,$_.j,_6v);$_.i=$_.i+9;$_.j=$_.j+6;break}$_.char1=$g($_.msg,$_.i);var _77=$_.i+1<$_.msglen?$g($_.msg,$_.i+1):-99;$_.char2=_77;var _7E=$_.i+2<$_.msglen?$g($_.msg,$_.i+2):-99;$_.char3=_7E;var _7H=$_[$_.cset];if($has(_7H,$_.char1)){var _7M=$_[$_.cset];$k[$j++]=$_.char1;$k[$j++]=_7M;$_.enc();$_.i=$_.i+1;break}if($eq($_.cset,"seta")&&$has($_.setb,$_.char1)){if($has($_.setb,$_.char2)){$k[$j++]=$_.maxicode_lb;$k[$j++]=$_.seta;$_.enc();$_.cset="setb"}else{$k[$j++]=$_.maxicode_sb;$k[$j++]=$_.seta;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.setb;$_.enc();$_.i=$_.i+1}break}if($eq($_.cset,"setb")&&$has($_.seta,$_.char1)){var _7e=$_.seta;var _7f=$_.msg;var _7g=$_.i;var _7h=$_.msglen;var _7i=$_.i;var _7j=_7h-_7i;var _7k=4;if(4>_7h-_7i){var _=_7j;_7j=_7k;_7k=_}$k[$j++]="p";$k[$j++]=_7e;$k[$j++]=$G(_7f,_7g,_7k);$_.prefixinset();var _7m=$k[--$j];var _7n=$k[--$j];$_[_7n]=_7m;if($_.p==1){$k[$j++]=$_.maxicode_sa;$k[$j++]=$_.setb;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.seta;$_.enc();$_.i=$_.i+1}if($_.p==2){$k[$j++]=$_.maxicode_sa2;$k[$j++]=$_.setb;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.seta;$_.enc();$k[$j++]=$_.char2;$k[$j++]=$_.seta;$_.enc();$_.i=$_.i+2}if($_.p==3){$k[$j++]=$_.maxicode_sa3;$k[$j++]=$_.setb;$_.enc();$k[$j++]=$_.char1;$k[$j++]=$_.seta;$_.enc();$k[$j++]=$_.char2;$k[$j++]=$_.seta;$_.enc();$k[$j++]=$_.char3;$k[$j++]=$_.seta;$_.enc();$_.i=$_.i+3}if($_.p>=4){$k[$j++]=$_.maxicode_la;$k[$j++]=$_.setb;$_.enc();$_.cset="seta"}break}if($has($_.seta,$_.char1)){var _8F=$_[$_.cset];$k[$j++]=$_.maxicode_la;$k[$j++]=_8F;$_.enc();$_.cset="seta";break}if($has($_.setb,$_.char1)){var _8K=$_[$_.cset];$k[$j++]=$_.maxicode_lb;$k[$j++]=_8K;$_.enc();$_.cset="setb";break}if($has($_.setc,$_.char1)){$_.setx="setc";$_.sx=$_.maxicode_sc;$_.lkx=$_.maxicode_lkc}if($has($_.setd,$_.char1)){$_.setx="setd";$_.sx=$_.maxicode_sd;$_.lkx=$_.maxicode_lkd}if($has($_.sete,$_.char1)){$_.setx="sete";$_.sx=$_.maxicode_se;$_.lkx=$_.maxicode_lke}var _8V=$_[$_.setx];var _8W=$_.msg;var _8X=$_.i;var _8Y=$_.msglen;var _8Z=$_.i;var _8a=_8Y-_8Z;var _8b=4;if(4>_8Y-_8Z){var _=_8a;_8a=_8b;_8b=_}$k[$j++]="p";$k[$j++]=_8V;$k[$j++]=$G(_8W,_8X,_8b);$_.prefixinset();var _8d=$k[--$j];var _8e=$k[--$j];$_[_8e]=_8d;if($_.p==1){var _8i=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_8i;$_.enc();var _8l=$_[$_.setx];$k[$j++]=$_.char1;$k[$j++]=_8l;$_.enc();$_.i=$_.i+1}if($_.p==2){var _8q=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_8q;$_.enc();var _8t=$_[$_.setx];$k[$j++]=$_.char1;$k[$j++]=_8t;$_.enc();var _8w=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_8w;$_.enc();var _8z=$_[$_.setx];$k[$j++]=$_.char2;$k[$j++]=_8z;$_.enc();$_.i=$_.i+2}if($_.p==3){var _94=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_94;$_.enc();var _97=$_[$_.setx];$k[$j++]=$_.char1;$k[$j++]=_97;$_.enc();var _9A=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_9A;$_.enc();var _9D=$_[$_.setx];$k[$j++]=$_.char2;$k[$j++]=_9D;$_.enc();var _9G=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_9G;$_.enc();var _9J=$_[$_.setx];$k[$j++]=$_.char3;$k[$j++]=_9J;$_.enc();$_.i=$_.i+3}if($_.p>=4){var _9O=$_[$_.cset];$k[$j++]=$_.sx;$k[$j++]=_9O;$_.enc();var _9R=$_[$_.setx];$k[$j++]=$_.lkx;$k[$j++]=_9R;$_.enc();$_.cset=$_.setx}break}}$_.encmsg=$G($_.out,0,$_.j);var _9X=$_[$_.cset];$_.padval=$g(_9X,$_.maxicode_pad)}if($eq($_.encoding,"new")){$_.haveb=false;$_.havec=false;$_.haved=false;$_.havee=false;$_.havei=false;for(var _9c=0,_9b=$_.msglen-1;_9c<=_9b;_9c+=1){$_.c=$g($_.msg,_9c);if($_.c<=-1e6){$_.havei=true}else{if(($_.c<28||$_.c>30)&&$_.c!=32){if($has($_.setb,$_.c)){$_.haveb=true}if($has($_.setc,$_.c)){$_.havec=true}if($has($_.setd,$_.c)){$_.haved=true}if($has($_.sete,$_.c)){$_.havee=true}}}}$k[$j++]=Infinity;$k[$j++]=0;if($_.haveb){$k[$j++]=1}if($_.havee){$k[$j++]=4}if($_.havec){$k[$j++]=2}if($_.haved){$k[$j++]=3}$_.priority=$a();$_.priority_rest=$G($_.priority,1,$_.priority.length-1);$_.latch_sequence=$_.maxicode_latchseq;$_.latch_length=$_.maxicode_latchlen;$k[$j++]=Infinity;for(var _A2=0;_A2<16;_A2++){$k[$j++]=$a([0,0,0,0,0])}$_.best_length=$a();$k[$j++]=Infinity;for(var _A4=0;_A4<16;_A4++){$k[$j++]=$a([0,0,0,0,0])}$_.best_origin=$a();$k[$j++]=Infinity;for(var _A7=0,_A8=$_.msglen;_A7<_A8;_A7++){var _AA=[];for(var _A9=0;_A9<5;_A9++){_AA[_A9]=0}_AA=$a(_AA);$k[$j++]=_AA}$_.prior_code_set=$a();$k[$j++]=Infinity;for(var _AC=0,_AD=$_.msglen;_AC<_AD;_AC++){var _AF=[];for(var _AE=0;_AE<5;_AE++){_AF[_AE]=0}_AF=$a(_AF);$k[$j++]=_AF}$_.path_op=$a();$_.out_eci=function(){var _AH=-$_.c-1e6;$k[$j++]=_AH;if(_AH<1024){var _AI=$k[--$j];$k[$j++]=_AI<32?2:3}else{var _AK=$k[--$j];$k[$j++]=_AK<32768?4:5}};$_.enc_eci=function(){var _AN=-$_.c-1e6;$k[$j++]=_AN;if(_AN>=32){var _AO=$k[$j-1];if(_AO<1024){var _AP=$k[--$j];$k[$j++]=$flr(_AP/64)|32;$k[$j++]=_AP&63}else{var _AQ=$k[$j-1];if(_AQ<32768){var _AR=$k[--$j];$k[$j++]=$flr(_AR/4096)|48;$k[$j++]=$flr(_AR/64)&63;$k[$j++]=_AR&63}else{var _AS=$k[--$j];$k[$j++]=$flr(_AS/262144)|56;$k[$j++]=$flr(_AS/4096)&63;$k[$j++]=$flr(_AS/64)&63;$k[$j++]=_AS&63}}}};$_.enc_ns=function(){var _AV=$G($_.msg,$_.n,9);var _Aa=0;for(var _AW=0,_AX=_AV.length;_AW<_AX;_AW++){_Aa=$f($g(_AV,_AW)-48+_Aa*10)}$k[$j++]=$flr(_Aa/16777216);$k[$j++]=$flr(_Aa/262144)&63;$k[$j++]=$flr(_Aa/4096)&63;$k[$j++]=$flr(_Aa/64)&63;$k[$j++]=_Aa&63};$_.enc_sha2=function(){var _Ai=$g($_.seta,$g($_.msg,$_.n+1));$k[$j++]=$g($_.seta,$_.c);$k[$j++]=_Ai};$_.enc_sha3=function(){var _Aq=$g($_.seta,$g($_.msg,$_.n+1));var _Av=$g($_.seta,$g($_.msg,$_.n+2));$k[$j++]=$g($_.seta,$_.c);$k[$j++]=_Aq;$k[$j++]=_Av};$k[$j++]=Infinity;if($_.havei){var _Ay=new Map([["can",function(){$k[$j++]=$_.c<=-1e6}],["intake",1],["output",function(){$_.out_eci()}],["sets",31],["enc",function(){$k[$j++]=27;$_.enc_eci()}]]);$k[$j++]=_Ay}var _B0=new Map([["can",function(){$k[$j++]=$_.digits>=9}],["intake",9],["output",function(){$k[$j++]=6}],["sets",31],["enc",function(){$k[$j++]=31;$_.enc_ns()}]]);var _B7=new Map([["can",function(){$k[$j++]=$has($_.seta,$_.c)}],["intake",1],["output",function(){$k[$j++]=1}],["sets",1],["enc",function(){$k[$j++]=$g($_.seta,$_.c)}]]);$k[$j++]=_B0;$k[$j++]=_B7;if($_.haveb){var _BF=new Map([["can",function(){$k[$j++]=$has($_.setb,$_.c)}],["intake",1],["output",function(){$k[$j++]=1}],["sets",2],["enc",function(){$k[$j++]=$g($_.setb,$_.c)}]]);$k[$j++]=_BF}if($_.havec){var _BN=new Map([["can",function(){$k[$j++]=$has($_.setc,$_.c)}],["intake",1],["output",function(){$k[$j++]=1}],["sets",4],["enc",function(){$k[$j++]=$g($_.setc,$_.c)}]]);$k[$j++]=_BN}if($_.haved){var _BV=new Map([["can",function(){$k[$j++]=$has($_.setd,$_.c)}],["intake",1],["output",function(){$k[$j++]=1}],["sets",8],["enc",function(){$k[$j++]=$g($_.setd,$_.c)}]]);$k[$j++]=_BV}if($_.havee){var _Bd=new Map([["can",function(){$k[$j++]=$has($_.sete,$_.c)}],["intake",1],["output",function(){$k[$j++]=1}],["sets",16],["enc",function(){$k[$j++]=$g($_.sete,$_.c)}]]);$k[$j++]=_Bd}var _Bi=new Map([["can",function(){$k[$j++]=$_.num_a>=1}],["intake",1],["output",function(){$k[$j++]=2}],["sets",2],["enc",function(){$k[$j++]=59;$k[$j++]=$g($_.seta,$_.c)}]]);var _Bk=new Map([["can",function(){$k[$j++]=$_.num_a>=2}],["intake",2],["output",function(){$k[$j++]=3}],["sets",2],["enc",function(){$k[$j++]=56;$_.enc_sha2()}]]);var _Bm=new Map([["can",function(){$k[$j++]=$_.num_a>=3}],["intake",3],["output",function(){$k[$j++]=4}],["sets",2],["enc",function(){$k[$j++]=57;$_.enc_sha3()}]]);$k[$j++]=_Bi;$k[$j++]=_Bk;$k[$j++]=_Bm;if($_.haveb){var _Bu=new Map([["can",function(){$k[$j++]=$has($_.setb,$_.c)}],["intake",1],["output",function(){$k[$j++]=2}],["sets",1],["enc",function(){$k[$j++]=59;$k[$j++]=$g($_.setb,$_.c)}]]);$k[$j++]=_Bu}if($_.havec){var _C2=new Map([["can",function(){$k[$j++]=$has($_.setc,$_.c)}],["intake",1],["output",function(){$k[$j++]=2}],["sets",27],["enc",function(){$k[$j++]=60;$k[$j++]=$g($_.setc,$_.c)}]]);$k[$j++]=_C2}if($_.haved){var _CA=new Map([["can",function(){$k[$j++]=$has($_.setd,$_.c)}],["intake",1],["output",function(){$k[$j++]=2}],["sets",23],["enc",function(){$k[$j++]=61;$k[$j++]=$g($_.setd,$_.c)}]]);$k[$j++]=_CA}if($_.havee){var _CI=new Map([["can",function(){$k[$j++]=$has($_.sete,$_.c)}],["intake",1],["output",function(){$k[$j++]=2}],["sets",15],["enc",function(){$k[$j++]=62;$k[$j++]=$g($_.sete,$_.c)}]]);$k[$j++]=_CI}$_.op_tab=$a();var _CJ=$_.op_tab;$k[$j++]=0;for(var _CK=0,_CL=_CJ.length;_CK<_CL;_CK++){var _CN=$k[--$j];$p($g(_CJ,_CK),"idx",_CN);$k[$j++]=_CN+1}$k[$j-1]=Infinity;var _CO=$a([1,2,4,8,16]);for(var _CP=0,_CQ=_CO.length;_CP<_CQ;_CP++){$_.n=$g(_CO,_CP);$k[$j++]=Infinity;var _CS=$_.op_tab;for(var _CT=0,_CU=_CS.length;_CT<_CU;_CT++){var _CV=$g(_CS,_CT);$k[$j++]=_CV;if($an($g(_CV,"sets"),$_.n)==0){$j--}}var _CY=$a();$k[$j++]=_CY}$_.code_set_operations=$a();$_.get_best_length=function(){$_.latch_length_s=$g($_.latch_length,$_.state);$k[$j++]=$_.maxicode_max_int;$F($g($_.code_set_operations,$_.state),function(){var _Cf=$k[--$j];$_.op=_Cf;if($g($_.op,"can")()===true){return true}var _Ci=$k[--$j];if(_Ci){$_.m=$f($_.n-$g($_.op,"intake"))&15;$_.org=$g($g($_.best_origin,$_.m),$_.state);$k[$j++]=$f($g($g($_.best_length,$_.m),$_.org)+$g($_.latch_length_s,$_.org));if($g($_.op,"output")()===true){return true}var _D1=$k[--$j];var _D2=$k[--$j];var _D3=$k[$j-1];$k[$j++]=$f(_D2+_D1);if(_D3>$f(_D2+_D1)){var _D4=$k[--$j];var _D5=$k[--$j];$p($_.path_op_0,$_.state,$g($_.op,"idx"));$p($_.prior_code_set_0,$_.state,$_.org);$k[$j++]=_D4;$k[$j++]=_D5}$j--}})};$_.get_best_origin=function(){$_.latch_length_s=$g($_.latch_length,$_.state);$_.orglen=$f($g($_.best_length_0,0)+$g($_.latch_length_s,0));var _DK=$_.priority_rest;$k[$j++]=0;for(var _DL=0,_DM=_DK.length;_DL<_DM;_DL++){$_.p=$g(_DK,_DL);var _DU=$f($g($_.best_length_0,$_.p)+$g($_.latch_length_s,$_.p));$k[$j++]=_DU;if(_DU<$_.orglen){var _DW=$k[--$j];$_.orglen=_DW;$k[$j-1]=$_.p}else{$j--}}};$_.digits=0;$_.num_a=0;for(var _Da=0,_DZ=$_.msglen-1;_Da<=_DZ;_Da+=1){$_.n=_Da;$_.c=$g($_.msg,$_.n);var _Dh=$_.c>=48&&$_.c<58?$_.digits+1:0;$_.digits=_Dh;var _Dl=$has($_.seta,$_.c);var _Dn=_Dl?$_.num_a+1:0;$_.num_a=_Dn;$_.path_op_0=$g($_.path_op,$_.n);$_.prior_code_set_0=$g($_.prior_code_set,$_.n);$_.best_length_0=$g($_.best_length,$_.n&15);$_.best_origin_0=$g($_.best_origin,$_.n&15);var _E1=$_.priority;for(var _E2=0,_E3=_E1.length;_E2<_E3;_E2++){$_.state=$g(_E1,_E2);$k[$j++]=$_.best_length_0;$k[$j++]=$_.state;$_.get_best_length();var _E7=$k[--$j];var _E8=$k[--$j];var _E9=$k[--$j];$p(_E9,_E8,_E7)}var _EA=$_.priority;for(var _EB=0,_EC=_EA.length;_EB<_EC;_EB++){$_.state=$g(_EA,_EB);$k[$j++]=$_.best_origin_0;$k[$j++]=$_.state;$_.get_best_origin();var _EG=$k[--$j];var _EH=$k[--$j];var _EI=$k[--$j];$p(_EI,_EH,_EG)}}$_.n=$_.msglen;var _EK=$_.priority;$k[$j++]=0;$k[$j++]=$_.maxicode_max_int;for(var _EL=0,_EM=_EK.length;_EL<_EM;_EL++){var _EN=$g(_EK,_EL);var _EP=$g($_.best_length_0,_EN);var _EQ=$k[$j-1];$k[$j++]=_EN;$k[$j++]=_EP;if($lt(_EP,_EQ)){$r(4,2)}$j-=2}var _ER=$k[--$j];$_.j=_ER;var _ES=$k[--$j];$_.state=_ES;var _EW=$g($_.maxicode_pad_code,$_.state)==0?0:$_.state;$_.final_code_set=_EW;if($g($_.maxicode_pad_code,$_.state)==0&&$lt($_.j,$_.maxlen)){var _Ed=$a($_.j+1);$p(_Ed,$_.j,58);$k[$j++]=_Ed}else{$k[$j++]=$a($_.j)}$_.padval=$g($_.maxicode_pad_code,$_.final_code_set);$_.len=$_.j;for(;;){if($_.n<=0){break}$_.pcs=$g($g($_.prior_code_set,$_.n-1),$_.state);$_.op_idx=$g($g($_.path_op,$_.n-1),$_.state);$_.op=$g($_.op_tab,$_.op_idx);$_.n=$f($_.n-$g($_.op,"intake"));$_.c=$g($_.msg,$_.n);$_.enc=$g($_.op,"enc");$k[$j++]=Infinity;if($_.enc()===true){break}$_.output=$a();$_.latch=$g($g($_.latch_sequence,$_.state),$_.pcs);$_.len=$f($_.len-$_.latch.length-$_.output.length);var _FF=$k[--$j];var _FG=$_.len;var _FH=$_.latch;$P(_FF,_FG,_FH);$P(_FF,$f(_FG+_FH.length),$_.output);$_.state=$_.pcs;$k[$j++]=_FF}var _FK=$k[--$j];$_.encmsg=_FK}var _FS=$_.sam!=-1?$a([$g($_.seta,$_.maxicode_pad),(~~($_.sam/10)-1)*8+($_.sam%10-1)]):$a([]);$_.sami=_FS;$k[$j++]=Infinity;$q($_.sami);$q($_.encmsg);$_.encmsg=$a();if($_.mode==2||$_.mode==3){if($_.encmsg.length>84){$k[$j++]="bwipp.maxicodeMode23TooLong#29240";$k[$j++]="The secondary message is too long";bwipp_raiseerror()}var _Fa=$Z($s(4),"0000");var _Fd=$R($s(4),~~$_.mode,2);$P(_Fa,4-_Fd.length,_Fd);$_.mdb=_Fa;var _Ff=$Z($s(10),"0000000000");var _Fi=$R($s(10),$i($_.ccode),2);$P(_Ff,10-_Fi.length,_Fi);$_.ccb=_Ff;var _Fk=$Z($s(10),"0000000000");var _Fn=$R($s(10),$i($_.scode),2);$P(_Fk,10-_Fn.length,_Fn);$_.scb=_Fk;$_.pcb=$Z($s(36),"000000000000000000000000000000000000");if($_.mode==2){if($eq($_.ccode,"840")&&$_.pcode.length==5){var _Fu=$Z($s(9)," 0000");$P(_Fu,0,$_.pcode);$_.pcode=_Fu}var _Fz=$R($s(6),$_.pcode.length,2);$P($_.pcb,6-_Fz.length,_Fz);var _G3=$R($s(30),$i($_.pcode),2);$P($_.pcb,36-_G3.length,_G3)}else{$k[$j++]=Infinity;var _G5=$Z($s(6)," ");var _GB=0;var _GA=$_.pcode.length>6?$G($_.pcode,0,6):$_.pcode;$P(_G5,_GB,_GA);$F(_G5,function(){var _GF=$k[--$j];$k[$j++]=$g($_.seta,_GF)});$_.pccw=$a();for(var _GH=0;_GH<=5;_GH+=1){$_.i=_GH;var _GN=$R($s(6),$g($_.pccw,$_.i),2);$P($_.pcb,6*$_.i+6-_GN.length,_GN)}}$_.scm=$s(60);$P($_.scm,2,$_.mdb);$P($_.scm,38,$G($_.pcb,0,4));$P($_.scm,30,$G($_.pcb,4,6));$P($_.scm,24,$G($_.pcb,10,6));$P($_.scm,18,$G($_.pcb,16,6));$P($_.scm,12,$G($_.pcb,22,6));$P($_.scm,6,$G($_.pcb,28,6));$P($_.scm,0,$G($_.pcb,34,2));$P($_.scm,52,$G($_.ccb,0,2));$P($_.scm,42,$G($_.ccb,2,6));$P($_.scm,36,$G($_.ccb,8,2));$P($_.scm,54,$G($_.scb,0,6));$P($_.scm,48,$G($_.scb,6,4));$_.pri=$a([0,0,0,0,0,0,0,0,0,0]);for(var _H2=0;_H2<=59;_H2+=1){$_.i=_H2;$_.ps=~~($_.i/6);$_.ep=$pow(2,5-$_.i%6)*($g($_.scm,$_.i)-48);$p($_.pri,$_.ps,$f($g($_.pri,$_.ps)+$_.ep))}$k[$j++]=Infinity;for(var _HE=0;_HE<84;_HE++){$k[$j++]=$_.padval}$_.sec=$a();$P($_.sec,0,$_.encmsg)}if($_.mode==4||$_.mode==5||$_.mode==6||$_.mode==-1){if($_.mode==-1){var _HO=$_.encmsg.length<=77?5:4;$_.mode=_HO}$k[$j++]=Infinity;var _HQ=$_.mode==5?78:94;for(var _HR=0,_HS=_HQ;_HR<_HS;_HR++){$k[$j++]=$_.padval}$_.cws=$a();if($_.encmsg.length>$_.cws.length-1){$k[$j++]="bwipp.maxicodeMode56TooLong#29325";$k[$j++]="The message is too long";bwipp_raiseerror()}$p($_.cws,0,$_.mode);$P($_.cws,1,$_.encmsg);$_.pri=$G($_.cws,0,10);$_.sec=$G($_.cws,10,$_.cws.length-10)}$_.rscodes=function(){var _Hf=$k[--$j];$_.rsnum=_Hf;var _Hg=$k[--$j];$_.cwb=_Hg;$k[$j++]="coeffs";$k[$j++]=$_.rsnum;$_.maxicode_coeffscachefetch();var _Hi=$k[--$j];var _Hj=$k[--$j];$_[_Hj]=_Hi;$_.lfsr=$a($_.rsnum);var _Hm=$_.cwb;var _Hn=$_.coeffs;var _Ho=$_.rsnum;var _Hp=$_.lfsr;$k[$j++]=function(){var _Hr=$k[--$j];$k[$j++]=$g($_.cwb,_Hr)};$k[$j++]=_Hm.length;$k[$j++]=_Hn;$k[$j++]=_Ho;$k[$j++]=_Hp;$k[$j++]=$_.maxicode_rsprod;bwipp_rsecbinary();$k[$j++]=$_.lfsr};$k[$j++]=Infinity;for(var _Hw=0,_Hv=$_.sec.length-1;_Hw<=_Hv;_Hw+=2){$k[$j++]=$g($_.sec,_Hw)}$_.seco=$a();$k[$j++]=Infinity;for(var _I1=1,_I0=$_.sec.length-1;_I1<=_I0;_I1+=2){$k[$j++]=$g($_.sec,_I1)}$_.sece=$a();var _I5=$_.sec.length==84?20:28;$_.scodes=_I5;$k[$j++]="secochk";$k[$j++]=$_.seco;$k[$j++]=$_.scodes;$_.rscodes();var _I8=$k[--$j];var _I9=$k[--$j];$_[_I9]=_I8;$k[$j++]="secechk";$k[$j++]=$_.sece;$k[$j++]=$_.scodes;$_.rscodes();var _IC=$k[--$j];var _ID=$k[--$j];$_[_ID]=_IC;$k[$j++]=Infinity;for(var _IG=0,_IF=$_.scodes-1;_IG<=_IF;_IG+=1){$k[$j++]=$g($_.secochk,_IG);$k[$j++]=$g($_.secechk,_IG)}$_.secchk=$a();$k[$j++]="prichk";$k[$j++]=$_.pri;$k[$j++]=10;$_.rscodes();var _IM=$k[--$j];var _IN=$k[--$j];$_[_IN]=_IM;$k[$j++]=Infinity;$q($_.pri);$q($_.prichk);$q($_.sec);$q($_.secchk);$_.codewords=$a();if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#29396";$k[$j++]=$_.codewords;bwipp_raiseerror()}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#29400";$k[$j++]=Infinity;$q($_.prichk);$q($_.secchk);var _IZ=$a();$k[$j++]=_IZ;bwipp_raiseerror()}$k[$j++]=Infinity;for(var _Ia=0;_Ia<864;_Ia++){$k[$j++]=0}$_.mods=$a();for(var _Ib=0;_Ib<=143;_Ib+=1){$_.i=_Ib;$k[$j++]=Infinity;var _Ig=$R($s(6),$g($_.codewords,$_.i),2);for(var _Ih=0,_Ii=_Ig.length;_Ih<_Ii;_Ih++){$k[$j++]=$g(_Ig,_Ih)-48}$_.cw=$a();$P($_.mods,6*$_.i+(6-$_.cw.length),$_.cw)}$_.pixs=$a(864);$_.j=0;for(var _Ir=0,_Iq=$_.mods.length-1;_Ir<=_Iq;_Ir+=1){$_.i=_Ir;if($g($_.mods,$_.i)==1){$p($_.pixs,$_.j,$g($_.maxicode_modmap,$_.i));$_.j=$_.j+1}}$k[$j++]=Infinity;$q($G($_.pixs,0,$_.j));$k[$j++]=28;$k[$j++]=29;$k[$j++]=280;$k[$j++]=281;$k[$j++]=311;$k[$j++]=457;$k[$j++]=488;$k[$j++]=500;$k[$j++]=530;$k[$j++]=670;$k[$j++]=700;$k[$j++]=677;$k[$j++]=707;$_.pixs=$a();$k[$j++]=new Map([["ren","renmaximatrix"],["pixs",$_.pixs],["borderleft",1],["borderright",1],["bordertop",1],["borderbottom",1],["opt",$_.options]]);if($_._render){bwipp_renmaximatrix()}$_=$__}function bwipp_azteccode(){if(!bwipp_azteccode.globals){var $__=$_;$_=bwipp_azteccode.globals={};$_.azteccode_u=0;$_.azteccode_l=1;$_.azteccode_m=2;$_.azteccode_p=3;$_.azteccode_d=4;$_.azteccode_b=5;$_.azteccode_lu=-2;$_.azteccode_ll=-3;$_.azteccode_lm=-4;$_.azteccode_lp=-5;$_.azteccode_ld=-6;$_.azteccode_su=-7;$_.azteccode_sp=-8;$_.azteccode_sb=-9;$_.azteccode_fl=-10;$_.azteccode_p2=-11;$_.azteccode_p3=-12;$_.azteccode_p4=-13;$_.azteccode_p5=-14;$_.lu=$_.azteccode_lu;$_.ll=$_.azteccode_ll;$_.lm=$_.azteccode_lm;$_.lp=$_.azteccode_lp;$_.ld=$_.azteccode_ld;$_.su=$_.azteccode_su;$_.sp=$_.azteccode_sp;$_.sb=$_.azteccode_sb;$_.fl=$_.azteccode_fl;$_.p2=$_.azteccode_p2;$_.p3=$_.azteccode_p3;$_.p4=$_.azteccode_p4;$_.p5=$_.azteccode_p5;$_.azteccode_charmaps=$a([$a([$_.sp,$_.sp,$_.sp,$_.fl,$_.sp]),$a([32,32,32,13,32]),$a(["A","a",1,$_.p2,"0"]),$a(["B","b",2,$_.p3,"1"]),$a(["C","c",3,$_.p4,"2"]),$a(["D","d",4,$_.p5,"3"]),$a(["E","e",5,"!","4"]),$a(["F","f",6,'"',"5"]),$a(["G","g",7,"#","6"]),$a(["H","h",8,"$","7"]),$a(["I","i",9,"%","8"]),$a(["J","j",10,"&","9"]),$a(["K","k",11,"'",","]),$a(["L","l",12,40,"."]),$a(["M","m",13,41,$_.lu]),$a(["N","n",27,"*",$_.su]),$a(["O","o",28,"+",-99]),$a(["P","p",29,",",-99]),$a(["Q","q",30,"-",-99]),$a(["R","r",31,".",-99]),$a(["S","s","@","/",-99]),$a(["T","t",92,":",-99]),$a(["U","u","^",";",-99]),$a(["V","v","_","<",-99]),$a(["W","w","`","=",-99]),$a(["X","x","|",">",-99]),$a(["Y","y","~","?",-99]),$a(["Z","z",127,"[",-99]),$a([$_.ll,$_.su,$_.ll,"]",-99]),$a([$_.lm,$_.lm,$_.lu,"{",-99]),$a([$_.ld,$_.ld,$_.lp,"}",-99]),$a([$_.sb,$_.sb,$_.sb,$_.lu,-99])]);var _1C=new Map([["\r\n",$_.p2],[". ",$_.p3],[", ",$_.p4],[": ",$_.p5]]);$_.azteccode_pcomp=_1C;$_.azteccode_latlen=$a([$a([0,5,5,10,5,10]),$a([9,0,5,10,5,10]),$a([5,5,0,5,10,10]),$a([5,10,10,0,10,15]),$a([4,9,9,14,0,14]),$a([0,0,0,0,0,0])]);$_.azteccode_latseq=$a([$a([$a([]),$a([$_.ll]),$a([$_.lm]),$a([$_.lm,$_.lp]),$a([$_.ld]),$a([$_.sb])]),$a([$a([$_.ld,$_.lu]),$a([]),$a([$_.lm]),$a([$_.lm,$_.lp]),$a([$_.ld]),$a([$_.sb])]),$a([$a([$_.lu]),$a([$_.ll]),$a([]),$a([$_.lp]),$a([$_.lu,$_.ld]),$a([$_.sb])]),$a([$a([$_.lu]),$a([$_.lu,$_.ll]),$a([$_.lu,$_.lm]),$a([]),$a([$_.lu,$_.ld]),$a([$_.lu,$_.sb])]),$a([$a([$_.lu]),$a([$_.lu,$_.ll]),$a([$_.lu,$_.lm]),$a([$_.lu,$_.lm,$_.lp]),$a([]),$a([$_.lu,$_.sb])]),$a([$a([$_.lu]),$a([$_.ll]),$a([$_.lm]),$a([]),$a([]),$a([])])]);$_.azteccode_e=1e6;$_.e=$_.azteccode_e;$_.azteccode_shftlen=$a([$a([$_.e,$_.e,$_.e,5,$_.e]),$a([5,$_.e,$_.e,5,$_.e]),$a([$_.e,$_.e,$_.e,5,$_.e]),$a([$_.e,$_.e,$_.e,$_.e,$_.e]),$a([4,$_.e,$_.e,4,$_.e])]);$_.azteccode_metrics=$a([$a(["rune",0,0,0,6]),$a(["compact",1,1,17,6]),$a(["full",1,1,21,6]),$a(["compact",2,0,40,6]),$a(["full",2,1,48,6]),$a(["compact",3,0,51,8]),$a(["full",3,1,60,8]),$a(["compact",4,0,76,8]),$a(["full",4,1,88,8]),$a(["full",5,1,120,8]),$a(["full",6,1,156,8]),$a(["full",7,1,196,8]),$a(["full",8,1,240,8]),$a(["full",9,1,230,10]),$a(["full",10,1,272,10]),$a(["full",11,1,316,10]),$a(["full",12,1,364,10]),$a(["full",13,1,416,10]),$a(["full",14,1,470,10]),$a(["full",15,1,528,10]),$a(["full",16,1,588,10]),$a(["full",17,1,652,10]),$a(["full",18,1,720,10]),$a(["full",19,1,790,10]),$a(["full",20,1,864,10]),$a(["full",21,1,940,10]),$a(["full",22,1,1020,10]),$a(["full",23,0,920,12]),$a(["full",24,0,992,12]),$a(["full",25,0,1066,12]),$a(["full",26,0,1144,12]),$a(["full",27,0,1224,12]),$a(["full",28,0,1306,12]),$a(["full",29,0,1392,12]),$a(["full",30,0,1480,12]),$a(["full",31,0,1570,12]),$a(["full",32,0,1664,12])]);$_.azteccode_rsparams=$a([$a([]),$a([]),$a([]),$a([]),$a([16,19]),$a([]),$a([64,67]),$a([]),$a([256,301]),$a([]),$a([1024,1033]),$a([]),$a([4096,4201])]);$_.azteccode_modemapfull=$a([$a([-5,7]),$a([-4,7]),$a([-3,7]),$a([-2,7]),$a([-1,7]),$a([1,7]),$a([2,7]),$a([3,7]),$a([4,7]),$a([5,7]),$a([7,5]),$a([7,4]),$a([7,3]),$a([7,2]),$a([7,1]),$a([7,-1]),$a([7,-2]),$a([7,-3]),$a([7,-4]),$a([7,-5]),$a([5,-7]),$a([4,-7]),$a([3,-7]),$a([2,-7]),$a([1,-7]),$a([-1,-7]),$a([-2,-7]),$a([-3,-7]),$a([-4,-7]),$a([-5,-7]),$a([-7,-5]),$a([-7,-4]),$a([-7,-3]),$a([-7,-2]),$a([-7,-1]),$a([-7,1]),$a([-7,2]),$a([-7,3]),$a([-7,4]),$a([-7,5])]);$_.azteccode_modemapcompact=$a([$a([-3,5]),$a([-2,5]),$a([-1,5]),$a([0,5]),$a([1,5]),$a([2,5]),$a([3,5]),$a([5,3]),$a([5,2]),$a([5,1]),$a([5,0]),$a([5,-1]),$a([5,-2]),$a([5,-3]),$a([3,-5]),$a([2,-5]),$a([1,-5]),$a([0,-5]),$a([-1,-5]),$a([-2,-5]),$a([-3,-5]),$a([-5,-3]),$a([-5,-2]),$a([-5,-1]),$a([-5,0]),$a([-5,1]),$a([-5,2]),$a([-5,3])]);$_.azteccode_encfuncs=$a(["encu","encl","encm","encp","encd"]);$_.azteccode_states=$a([$_.azteccode_u,$_.azteccode_l,$_.azteccode_m,$_.azteccode_p,$_.azteccode_d,$_.azteccode_b]);$_.azteccode_charsizes=$a([5,5,5,5,4,8]);$_.azteccode_coeffscachemax=551;$_.azteccode_coeffscachelimit=6e3;$_.azteccode_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _58=0,_59=$_.rsnc;_58<_59;_58++){$k[$j++]=0}$_.coeffs=$a();for(var _5C=1,_5B=$_.rsnc;_5C<=_5B;_5C+=1){var _5D=$_.coeffs;$p(_5D,_5C,$g(_5D,_5C-1));$k[$j++]=$g($_.rsalog,_5C);for(var _5H=_5C-1;_5H>=1;_5H-=1){var _5I=$_.coeffs;var _5K=$k[$j-1];$k[$j++]=_5I;$k[$j++]=_5H;$k[$j++]=$g(_5I,_5H);$k[$j++]=_5K;$_.rsprod();var _5M=$k[--$j];var _5N=$k[--$j];var _5P=$k[--$j];$p(_5P,_5N,$xo(_5M,$g($_.coeffs,_5N-1)))}var _5Q=$_.coeffs;var _5S=$k[$j-1];$k[$j++]=_5Q;$k[$j++]=0;$k[$j++]=$g(_5Q,0);$k[$j++]=_5S;$_.rsprod();var _5T=$k[--$j];var _5U=$k[--$j];var _5V=$k[--$j];$p(_5V,_5U,_5T);$j--}$k[$j++]=$_.coeffs};$k[$j++]=$_.azteccode_coeffscachemax;$k[$j++]=$_.azteccode_coeffscachelimit;bwipp_fifocache();var _5Z=$k[--$j];$_.azteccode_coeffscache=_5Z;$_.azteccode_coeffscachefetch=function(){var _5a=$k[--$j];var _5b=$k[--$j];var _5c=$g($_.azteccode_coeffscache,"fetch");$k[$j++]=$f(_5b+_5a*1e5);$k[$j++]=function(){$_.azteccode_gencoeffs()};$k[$j++]=function(){var _5d=$k[--$j];$k[$j++]=_5d.length};if(_5c()===true){return true}};$_.charvals=$a([new Map,new Map,new Map,new Map,new Map]);for(var _5f=0,_5e=$_.azteccode_charmaps.length-1;_5f<=_5e;_5f+=1){$_.i=_5f;$_.encs=$g($_.azteccode_charmaps,$_.i);for(var _5i=0;_5i<=4;_5i+=1){$_.j=_5i;var _5l=$g($_.encs,$_.j);var _5m=$t(_5l);if($eq(_5m,"stringtype")){_5l=$g(_5l,0)}$p($g($_.charvals,$_.j),_5l,$_.i)}}$_.charvals=$_.charvals;$k[$j++]=Infinity;for(var _5v=0,_5w=$_.azteccode_rsparams.length;_5v<_5w;_5v++){var _5x=$g($_.azteccode_rsparams,_5v);$k[$j++]=_5x;if(_5x.length!=0){var _5y=$k[--$j];$q(_5y);var _5z=$k[--$j];$_.rspm=_5z;var _60=$k[--$j];$_.rsgf=_60;$k[$j++]=Infinity;$k[$j++]=1;for(var _62=0,_63=$_.rsgf-1;_62<_63;_62++){var _64=$k[$j-1];var _65=_64*2;$k[$j++]=_65;if(_65>=$_.rsgf){var _68=$k[--$j];$k[$j++]=$xo(_68,$_.rspm)}}$_.rsalog=$a();$_.rslog=$a($_.rsgf);for(var _6D=1,_6C=$_.rsgf-1;_6D<=_6C;_6D+=1){$p($_.rslog,$g($_.rsalog,_6D),_6D)}$k[$j++]=$a([$_.rslog,$_.rsalog])}else{$k[$j-1]=$a([])}}var _6L=$a();$_.rstables=_6L;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_azteccode.globals);$_.dontdraw=false;$_.format="full";$_.readerinit=false;$_.layers=-1;$_.eclevel=23;$_.ecaddchars=3;$_.raw=false;$_.parse=false;$_.parsefnc=false;bwipp_processoptions();var _6M=$k[--$j];$_.options=_6M;var _6N=$k[--$j];$_.barcode=_6N;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.barcode.length==0){$k[$j++]="bwipp.aztecEmptyData#29798";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>2e4){$k[$j++]="bwipp.azteccodeinputTooLong#29802";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($_.eclevel<5||$_.eclevel>95){$k[$j++]="bwipp.aztecInvalidEClevel#29806";$k[$j++]="The EC percentage must be from 5 to 95";bwipp_raiseerror()}if($_.ecaddchars<3){$k[$j++]="bwipp.aztecInvalidECaddChars#29810";$k[$j++]="The number of additional EC codewords must be 3 or more";bwipp_raiseerror()}if($ne($_.format,"full")&&($ne($_.format,"compact")&&$ne($_.format,"rune"))){$k[$j++]="bwipp.aztecInvalidFormat#29814";$k[$j++]="The format must be either full, compact or rune";bwipp_raiseerror()}if($eq($_.format,"full")){if($_.readerinit&&($_.layers<1||$_.layers>22)){$k[$j++]="bwipp.aztecFullInitInvalidLayers#29819";$k[$j++]="Full-range symbols for reader programming must specify from 1 to 22 layers";bwipp_raiseerror()}if($_.layers!=-1&&($_.layers<1||$_.layers>32)){$k[$j++]="bwipp.aztecFullInvalidLayers#29822";$k[$j++]="Layers for full-range symbols must be from 1 to 32";bwipp_raiseerror()}}if($eq($_.format,"compact")){if($_.readerinit){if($_.layers!=-1&&$_.layers!=1){$k[$j++]="bwipp.aztecCompactInitInvalidLayers#29829";$k[$j++]="Compact symbols for reader programming must have 1 layer";bwipp_raiseerror()}$_.layers=1}if($_.layers!=-1&&($_.layers<1||$_.layers>4)){$k[$j++]="bwipp.aztecCompactInvalidLayers#29834";$k[$j++]="Layers for compact symbols must be from 1 to 4";bwipp_raiseerror()}}if($eq($_.format,"rune")){if($_.layers!=-1){$k[$j++]="bwipp.aztecRuneInvalidLayers#29840";$k[$j++]="It is not valid to specify layers for runes";bwipp_raiseerror()}if($_.readerinit){$k[$j++]="bwipp.aztecRuneReaderInit#29843";$k[$j++]="Reader initialisation is not compactible with Aztec Runes";bwipp_raiseerror()}if($_.barcode.length==0){$k[$j++]="bwipp.aztecRuneNotNumeric#29846";$k[$j++]="Aztec runes must be numeric";bwipp_raiseerror()}$F($_.barcode,function(){var _6r=$k[--$j];if(_6r<48||_6r>57){$k[$j++]="bwipp.aztecRuneNotNumeric#29850";$k[$j++]="Aztec runes must be numeric";bwipp_raiseerror()}});if($_.barcode.length>3){$k[$j++]="bwipp.aztecRuneInvalid#29854";$k[$j++]="Aztec runes must be 0 to 255";bwipp_raiseerror()}var _6u=$i($_.barcode);if(_6u<0||_6u>255){$k[$j++]="bwipp.aztecRuneInvalid#29857";$k[$j++]="Aztec runes must be 0 to 255";bwipp_raiseerror()}}$_.fn1=-1;$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true],["FNC1",$_.fn1]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _70=$k[--$j];$_.msg=_70;$_.msglen=$_.msg.length;$_.msgbits="";if($ne($_.format,"rune")&&$_.raw){$_.msgbits=$_.barcode}if($ne($_.format,"rune")&&!$_.raw){$_.charsize=function(){var _77=$k[$j-1];if(_77>=0){$j--;var _78=$k[--$j];$k[$j++]=$g($_.azteccode_charsizes,_78)}else{var _7A=$k[--$j];$k[$j-1]=_7A;var _7C=$k[$j-1];if(_7C==$_.fn1){$k[$j-1]=8}else{var _7E=$k[--$j];var _7F=-_7E-1e6;if(_7F==0){_7F=1}$k[$j++]=(~~($log(_7F)/$log(10))+1)*4+8}}};$_.e=$_.azteccode_e;$_.maxseqlen=$_.msglen*4;$k[$j++]=Infinity;for(var _7I=0;_7I<6;_7I++){$k[$j++]=$_.maxseqlen;try{var _7K=$k[--$j];$k[$j++]=$a(_7K);var _7M=false}catch(e){$k[$j++]=true}if(_7M){$k[$j-1]="bwipp.azteccodeInputTooLarge#29911";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}}$_.seq0=$a();$k[$j++]=Infinity;for(var _7N=0;_7N<6;_7N++){$k[$j++]=$_.maxseqlen;try{var _7P=$k[--$j];$k[$j++]=$a(_7P);var _7R=false}catch(e){$k[$j++]=true}if(_7R){$k[$j-1]="bwipp.azteccodeInputTooLarge#29914";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}}$_.seq1=$a();$_.len0=$a(6);for(var _7T=0;_7T<=5;_7T+=1){$p($_.len0,_7T,0)}$_.len1=$a(6);$_.curseq=$_.seq0;$_.curseqlen=$_.len0;$_.nxtseq=$_.seq1;$_.nxtseqlen=$_.len1;$_.curlen=$a([0,$_.e,$_.e,$_.e,$_.e,$_.e]);$_.backto=$_.azteccode_u;$_.lastchar="";$F($_.msg,function(){var _7g=$k[--$j];$_.char=_7g;for(;;){$_.imp=false;for(var _7h=0,_7i=$_.azteccode_states.length;_7h<_7i;_7h++){$_.x=$g($_.azteccode_states,_7h);for(var _7k=0,_7l=$_.azteccode_states.length;_7k<_7l;_7k++){$_.y=$g($_.azteccode_states,_7k);if($_.x!=$_.azteccode_b||$_.y==$_.backto){$_.cost=$f($g($_.curlen,$_.x)+$g($g($_.azteccode_latlen,$_.x),$_.y));if($_.cost<$g($_.curlen,$_.y)){$p($_.curlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.curseq,$_.y),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$F($g($g($_.azteccode_latseq,$_.x),$_.y),function(){$k[$j++]=$g($_.curseq,$_.y);$k[$j++]=$_.srclen;var _8O=$k[$j-3];var _8P=$k[$j-1];var _8Q=$k[$j-2];$j-=3;$p(_8Q,_8P,_8O);$_.srclen=$_.srclen+1});$p($_.curseqlen,$_.y,$_.srclen);if($_.y==$_.azteccode_b){var _8Z=$_.x==$_.azteccode_p||$_.x==$_.azteccode_d?$_.azteccode_u:$_.x;$_.backto=_8Z}$_.imp=true}}}}if(!$_.imp){break}}$_.nxtlen=$a([$_.e,$_.e,$_.e,$_.e,$_.e,$_.e]);for(var _8i=0;_8i<=5;_8i+=1){$p($_.nxtseqlen,_8i,0)}for(var _8k=0,_8l=$_.azteccode_states.length;_8k<_8l;_8k++){$_.x=$g($_.azteccode_states,_8k);for(;;){if($_.char>=0){if($_.x!=$_.azteccode_b){if(!$has($g($_.charvals,$_.x),$_.char)){break}}}else{if($_.x!=$_.azteccode_p){break}}$k[$j++]="cost";$k[$j++]=$g($_.curlen,$_.x);$k[$j++]=$_.x;$k[$j++]=$_.char;$_.charsize();var _90=$k[--$j];var _91=$k[--$j];var _92=$k[--$j];$_[_92]=$f(_91+_90);if($_.cost<$g($_.nxtlen,$_.x)){$p($_.nxtlen,$_.x,$_.cost);$_.srclen=$g($_.curseqlen,$_.x);if($_.srclen>0){$P($g($_.nxtseq,$_.x),0,$G($g($_.curseq,$_.x),0,$_.srclen))}$p($g($_.nxtseq,$_.x),$_.srclen,$_.char);$p($_.nxtseqlen,$_.x,$_.srclen+1)}if($_.x==$_.azteccode_b){break}for(var _9V=0,_9W=$_.azteccode_states.length;_9V<_9W;_9V++){$_.y=$g($_.azteccode_states,_9V);if($_.y==$_.azteccode_b){break}if($ne($_.x,$_.y)){var _9h=$g($g($_.azteccode_shftlen,$_.y),$_.x);$k[$j++]="cost";$k[$j++]=$f($g($_.curlen,$_.y)+_9h);$k[$j++]=$_.x;$k[$j++]=$_.char;$_.charsize();var _9k=$k[--$j];var _9l=$k[--$j];var _9m=$k[--$j];$_[_9m]=$f(_9l+_9k);if($_.cost<$g($_.nxtlen,$_.y)){$p($_.nxtlen,$_.y,$_.cost);$_.srclen=$g($_.curseqlen,$_.y);if($_.srclen>0){$P($g($_.nxtseq,$_.y),0,$G($g($_.curseq,$_.y),0,$_.srclen))}var _AD=$g($_.nxtseq,$_.y);var _AC=$_.srclen;var _AB=$_.x==$_.azteccode_u?$_.azteccode_su:$_.azteccode_sp;$p(_AD,_AC,_AB);$p($g($_.nxtseq,$_.y),$_.srclen+1,$_.char);$p($_.nxtseqlen,$_.y,$_.srclen+2)}}}break}}if($_.lastchar.length!=0&&$_.char>=0){var _AO=$s(2);$p(_AO,0,$_.lastchar);$p(_AO,1,$_.char);$_.pchars=_AO;if($has($_.azteccode_pcomp,$_.pchars)){for(var _AT=0,_AU=$_.azteccode_states.length;_AT<_AU;_AT++){$_.i=$g($_.azteccode_states,_AT);if($_.i==$_.azteccode_b){break}$_.inP=true;if($_.i==$_.azteccode_m){if($_.lastchar==13){$_.inP=false}}else{if($_.i==$_.azteccode_d){if($_.lastchar==44||$_.lastchar==46){$_.inP=false}}}$_.curseqilen=$g($_.curseqlen,$_.i);if($_.inP&&$lt($g($_.curlen,$_.i),$g($_.nxtlen,$_.i))){$_.lastld=false;$_.lastsp=false;$_.lastidx=-1;for(var _An=$_.curseqilen-1;_An>=0;_An-=1){$_.idx=_An;$_.ch=$g($g($_.curseq,$_.i),$_.idx);if($_.lastidx==-1){if($eq($_.ch,$_.lastchar)){$_.lastidx=$_.idx;if($_.idx>0){if($g($g($_.curseq,$_.i),$_.idx-1)==$_.azteccode_sp){$_.lastsp=true}}}}else{if($_.ch==$_.azteccode_sb){$_.lastidx=-1;break}if($_.ch<0&&$_.ch>=$_.azteccode_ld){if($_.i==$_.azteccode_p){if($_.ch==$_.azteccode_ld){$_.lastld=true}}else{if($_.ch!=$_.azteccode_lp){$_.inP=$_.lastsp}}break}}}if($_.inP&&$_.lastidx>=0){$p($_.nxtlen,$_.i,$g($_.curlen,$_.i));if($_.lastidx<$_.curseqilen-1){if($_.i==$_.azteccode_p){if($_.lastld){$p($_.nxtlen,$_.i,$g($_.nxtlen,$_.i)+1)}$_.srclen=$_.curseqilen;$P($g($_.nxtseq,$_.i),0,$G($g($_.curseq,$_.i),0,$_.lastidx));$P($g($_.nxtseq,$_.i),$_.lastidx,$G($g($_.curseq,$_.i),$_.lastidx+1,$f($_.srclen-$_.lastidx)-1));$p($g($_.nxtseq,$_.i),$_.srclen-1,$g($_.azteccode_pcomp,$_.pchars));$p($_.nxtseqlen,$_.i,$_.srclen)}else{if($_.curseqilen>0){$P($g($_.nxtseq,$_.i),0,$G($g($_.curseq,$_.i),0,$_.curseqilen))}$p($g($_.nxtseq,$_.i),$_.lastidx,$g($_.azteccode_pcomp,$_.pchars));$p($_.nxtseqlen,$_.i,$_.curseqilen)}}else{$_.srclen=$_.curseqilen-1;if($_.srclen>0){$P($g($_.nxtseq,$_.i),0,$G($g($_.curseq,$_.i),0,$_.srclen))}$p($g($_.nxtseq,$_.i),$_.srclen,$g($_.azteccode_pcomp,$_.pchars));$p($_.nxtseqlen,$_.i,$_.srclen+1)}}}}}}if($g($_.nxtseqlen,$_.azteccode_b)>0){$_.numbytes=0;for(var _CZ=0,_CY=$g($_.nxtseqlen,$_.azteccode_b)-1;_CZ<=_CY;_CZ+=1){var _Ce=$g($g($_.nxtseq,$_.azteccode_b),_CZ)==$_.azteccode_sb?0:$_.numbytes+1;$_.numbytes=_Ce}if($_.numbytes==32){$p($_.nxtlen,$_.azteccode_b,$g($_.nxtlen,$_.azteccode_b)+11)}}$_.curlen=$_.nxtlen;var _Ck=$_.curseq;$_.curseq=$_.nxtseq;$_.nxtseq=_Ck;var _Cm=$_.curseqlen;$_.curseqlen=$_.nxtseqlen;$_.nxtseqlen=_Cm;var _Cq=$_.char>=0?$_.char:"";$_.lastchar=_Cq});$_.minseq=$_.azteccode_e;for(var _Cs=0,_Ct=$_.azteccode_states.length;_Cs<_Ct;_Cs++){$_.i=$g($_.azteccode_states,_Cs);if($g($_.curlen,$_.i)<$_.minseq){$_.minseq=$g($_.curlen,$_.i);$_.seq=$G($g($_.curseq,$_.i),0,$g($_.curseqlen,$_.i))}}$_.tobin=function(){var _D9=$k[--$j];var _DA=$s(_D9);$k[$j++]=_DA;for(var _DC=0,_DB=_DA.length-1;_DC<=_DB;_DC+=1){var _DD=$k[$j-1];$p(_DD,_DC,48)}var _DE=$k[$j-1];$k[$j++]=_DE;var _DF=$k[$j-3];var _DG=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _DI=$R($s(_DG.length),_DF,2);$P(_DG,_DG.length-_DI.length,_DI)};$_.encu=function(){var _DL=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.azteccode_u),_DL);$k[$j++]=5;$_.tobin()};$_.encl=function(){var _DP=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.azteccode_l),_DP);$k[$j++]=5;$_.tobin()};$_.encm=function(){var _DT=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.azteccode_m),_DT);$k[$j++]=5;$_.tobin()};$_.encd=function(){var _DX=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.azteccode_d),_DX);$k[$j++]=4;$_.tobin()};$_.encp=function(){var _DZ=$k[$j-1];if(_DZ==$_.fn1){$k[$j-1]="00000000"}else{var _Db=$k[$j-1];if(_Db<=-1e6){var _Dc=$k[--$j];var _Dd=-_Dc-1e6;$k[$j++]=_Dd;if(_Dd==0){_Dd=1}var _Df=~~($log(_Dd)/$log(10));var _Dg=$s((_Df+1)*4+8);$P(_Dg,0,"00000");$k[$j++]=_Df;$k[$j++]=_Dg;$k[$j++]=_Dg;$k[$j++]=_Df+1;$k[$j++]=3;$_.tobin();var _Dh=$k[--$j];var _Di=$k[--$j];$P(_Di,5,_Dh);$r(3,1);$k[$j++]=-1;$k[$j++]=0;if($j<3)throw"--stack-underflow--";$j-=3;for(var _Dm=$k[$j],_Dn=$k[$j+1],_Dl=$k[$j+2];_Dn<0?_Dm>=_Dl:_Dm<=_Dl;_Dm+=_Dn){var _Do=$k[--$j];var _Dp=$k[$j-1];$k[$j++]=~~(_Do/10);$k[$j++]=_Dp;$k[$j++]=_Dm*4+8;$k[$j++]=_Do%10+2;$k[$j++]=4;$_.tobin();var _Dq=$k[--$j];var _Dr=$k[--$j];var _Ds=$k[--$j];$P(_Ds,_Dr,_Dq)}$j--}else{var _Dv=$k[--$j];$k[$j++]=$g($g($_.charvals,$_.azteccode_p),_Dv);$k[$j++]=5;$_.tobin()}}};$_.addtomsgbits=function(){var _Dx=$k[--$j];$_.v=_Dx;if($_.j+$_.v.length>$_.msgbits.length){$k[$j++]="bwipp.aztecNoValidSymbol#30180";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$P($_.msgbits,$_.j,$_.v);$_.j=$_.j+$_.v.length};$_.state=$_.azteccode_u;$k[$j++]=$_.minseq;try{var _E7=$k[--$j];$k[$j++]=$s(_E7);var _E9=false}catch(e){$k[$j++]=true}if(_E9){$k[$j-1]="bwipp.azteccodeInputTooLarge#30191";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _EA=$k[--$j];$_.msgbits=_EA;$_.i=0;$_.j=0;for(;;){if($_.i>=$_.seq.length){break}if($_.state!=$_.azteccode_b){$_.char=$g($_.seq,$_.i);var _EK=$_[$g($_.azteccode_encfuncs,$_.state)];$k[$j++]=$_.char;if(_EK()===true){break}$_.addtomsgbits();$_.i=$_.i+1;if($_.char==$_.azteccode_su||$_.char==$_.azteccode_sp){$k[$j++]=$g($_.seq,$_.i);if($_.char==$_.azteccode_su){$_.encu()}else{$_.encp()}$_.addtomsgbits();$_.i=$_.i+1}if($_.char==$_.azteccode_lu){$_.state=$_.azteccode_u}if($_.char==$_.azteccode_ll){$_.state=$_.azteccode_l}if($_.char==$_.azteccode_lm){$_.state=$_.azteccode_m}if($_.char==$_.azteccode_lp){$_.state=$_.azteccode_p}if($_.char==$_.azteccode_ld){$_.state=$_.azteccode_d}if($_.char==$_.azteccode_sb){$_.state=$_.azteccode_b}}else{$_.numbytes=0;for(;;){if($_.i+$_.numbytes>=$_.seq.length){break}if($g($_.seq,$_.i+$_.numbytes)<0){break}if($_.numbytes==2078){break}$_.numbytes=$_.numbytes+1}if($_.numbytes<=31){$k[$j++]=$_.numbytes;$k[$j++]=5;$_.tobin();$_.addtomsgbits()}else{$k[$j++]=0;$k[$j++]=5;$_.tobin();$_.addtomsgbits();$k[$j++]=$_.numbytes-31;$k[$j++]=11;$_.tobin();$_.addtomsgbits()}for(var _Em=0,_En=$_.numbytes;_Em<_En;_Em++){$k[$j++]=$g($_.seq,$_.i);$k[$j++]=8;$_.tobin();$_.addtomsgbits();$_.i=$_.i+1}if($_.i<$_.seq.length){$_.char=$g($_.seq,$_.i);$_.i=$_.i+1;if($_.char==$_.azteccode_lu){$_.state=$_.azteccode_u}if($_.char==$_.azteccode_ll){$_.state=$_.azteccode_l}if($_.char==$_.azteccode_lm){$_.state=$_.azteccode_m}}}}$_.msgbits=$G($_.msgbits,0,$_.j)}if($has($_.options,"debugbits")){$k[$j++]="bwipp.debugbits#30260";$k[$j++]=$_.msgbits;bwipp_raiseerror()}for(var _F7=0,_F8=$_.azteccode_metrics.length;_F7<_F8;_F7++){$_.m=$g($_.azteccode_metrics,_F7);$_.frmt=$g($_.m,0);$_.mlyr=$g($_.m,1);$_.icap=$g($_.m,2);$_.ncws=$g($_.m,3);$_.bpcw=$g($_.m,4);$_.numecw=$ceil($f($_.ncws*$_.eclevel/100+$_.ecaddchars));if($_.msgbits.length==0){$_.numecw=0}$_.numdcw=$f($_.ncws-$_.numecw);$_.okay=true;if($ne($_.format,$_.frmt)){$_.okay=false}if($_.readerinit&&$_.icap!=1){$_.okay=false}if($_.layers!=-1&&$_.layers!=$_.mlyr){$_.okay=false}if($ceil($_.msgbits.length/$_.bpcw)>$_.numdcw){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.aztecNoValidSymbol#30284";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$_.layers=$_.mlyr;$_.allzero=function(){var _Fd=$k[--$j];$k[$j++]=$eq(_Fd,$G("000000000000",0,_Fd.length))};$_.allones=function(){var _Ff=$k[--$j];$k[$j++]=$eq(_Ff,$G("111111111111",0,_Ff.length))};$_.cws=$a($_.ncws);$_.m=0;$_.c=0;for(;;){if($_.msgbits.length<=$_.m){break}if($_.msgbits.length-$_.m>=$_.bpcw){$_.cwb=$G($_.msgbits,$_.m,$_.bpcw-1);$_.cwf=$G($_.msgbits,$f($_.m+$_.bpcw)-1,1);$k[$j++]=$_.cwb;$_.allzero();var _Fx=$k[--$j];if(_Fx){$_.cwf="1";$_.m=$_.m-1}$k[$j++]=$_.cwb;$_.allones();var _G0=$k[--$j];if(_G0){$_.cwf="0";$_.m=$_.m-1}var _G2=$s(12);$P(_G2,0,$_.cwb);$P(_G2,$_.bpcw-1,$_.cwf);$_.cwb=$G(_G2,0,$_.bpcw)}else{$_.cwb=$G($_.msgbits,$_.m,$_.msgbits.length-$_.m);var _GE=$Z($s(12),"111111111111");$P(_GE,0,$_.cwb);$_.cwb=$G(_GE,0,$_.bpcw);$k[$j++]=$_.cwb;$_.allones();var _GJ=$k[--$j];if(_GJ){$P($_.cwb,$_.cwb.length-1,"0")}}$_.cw=0;for(var _GO=0,_GN=$_.bpcw-1;_GO<=_GN;_GO+=1){$_.i=_GO;$_.cw=$f($_.cw+$pow(2,$f($_.bpcw-$_.i)-1)*($g($_.cwb,$_.i)-48))}$p($_.cws,$_.c,$_.cw);$_.m=$f($_.m+$_.bpcw);$_.c=$_.c+1}$_.cws=$G($_.cws,0,$_.c);if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#30327";$k[$j++]=$_.cws;bwipp_raiseerror()}$_.rscodes=function(){var _Gh=$k[--$j];$_.rsbt=_Gh;var _Gi=$k[--$j];$_.rsnc=_Gi;var _Gj=$k[--$j];$_.rscws=_Gj;$q($g($_.azteccode_rsparams,$_.rsbt));var _Gm=$k[--$j];$_.rspm=_Gm;var _Gn=$k[--$j];$_.rsgf_1=_Gn-1;$q($g($_.rstables,$_.rsbt));var _Gr=$k[--$j];$_.rsalog=_Gr;var _Gs=$k[--$j];$_.rslog=_Gs;$_.rsprod=function(){var _Gt=$k[$j-1];if(_Gt!=0){var _Gv=$k[$j-2];if(_Gv!=0){var _Gx=$k[--$j];var _Gz=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_Gx)+$g($_.rslog,_Gz))%$_.rsgf_1)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$k[$j++]="coeffs";$k[$j++]=$_.rsnc;$k[$j++]=$_.rsbt;$_.azteccode_coeffscachefetch();var _H7=$k[--$j];var _H8=$k[--$j];$_[_H8]=_H7;$_.lfsr=$a($_.rsnc);var _HB=$_.rscws;var _HC=$_.coeffs;var _HD=$_.rsnc;var _HE=$_.lfsr;$k[$j++]=function(){var _HG=$k[--$j];$k[$j++]=$g($_.rscws,_HG)};$k[$j++]=_HB.length;$k[$j++]=_HC;$k[$j++]=_HD;$k[$j++]=_HE;$k[$j++]=$_.rsprod;bwipp_rsecbinary();$k[$j++]=$_.lfsr};if($eq($_.format,"full")){$_.mode=($_.layers-1)*2048+($_.cws.length-1);if($_.readerinit){$_.mode=$_.mode|1024}$_.mode=$a([$flr(($_.mode&61440)/4096),$flr(($_.mode&3840)/256),$flr(($_.mode&240)/16),$_.mode&15]);$k[$j++]=Infinity;$q($_.mode);$k[$j++]=$_.mode;$k[$j++]=6;$k[$j++]=4;$_.rscodes();var _HU=$k[--$j];$q(_HU);$_.mode=$a()}if($eq($_.format,"compact")){$_.mode=($_.layers-1)*64+($_.cws.length-1);if($_.readerinit){$_.mode=$_.mode|32}$_.mode=$a([$flr(($_.mode&240)/16),$_.mode&15]);$k[$j++]=Infinity;$q($_.mode);$k[$j++]=$_.mode;$k[$j++]=5;$k[$j++]=4;$_.rscodes();var _He=$k[--$j];$q(_He);$_.mode=$a()}if($eq($_.format,"rune")){$_.mode=$i($_.barcode);$_.mode=$a([$flr(($_.mode&240)/16),$_.mode&15]);$k[$j++]=Infinity;$q($_.mode);$k[$j++]=$_.mode;$k[$j++]=5;$k[$j++]=4;$_.rscodes();var _Hl=$k[--$j];$q(_Hl);$_.mode=$a();$k[$j++]=Infinity;var _Hm=$_.mode;for(var _Hn=0,_Ho=_Hm.length;_Hn<_Ho;_Hn++){$k[$j++]=$g(_Hm,_Hn)^10}$_.mode=$a()}$_.modebits=$s($_.mode.length*4);for(var _Hu=0,_Ht=$_.modebits.length-1;_Hu<=_Ht;_Hu+=1){$P($_.modebits,_Hu,"0")}for(var _Hy=0,_Hx=$_.mode.length-1;_Hy<=_Hx;_Hy+=1){$_.i=_Hy;var _I4=$R($s(4),$g($_.mode,$_.i),2);$P($_.modebits,4-_I4.length+4*$_.i,_I4)}$k[$j++]="ecws";$k[$j++]=$_.cws;$k[$j++]=$f($_.ncws-$_.cws.length);$k[$j++]=$_.bpcw;$_.rscodes();var _IA=$k[--$j];var _IB=$k[--$j];$_[_IB]=_IA;if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#30416";$k[$j++]=$_.ecws;bwipp_raiseerror()}$k[$j++]=Infinity;$q($_.cws);$q($_.ecws);$_.cws=$a();if($eq($_.format,"full")){$_.databits=$s($f($_.layers*$_.layers*16+$_.layers*112))}else{$_.databits=$s($f($_.layers*$_.layers*16+$_.layers*88))}for(var _IS=0,_IR=$_.databits.length-1;_IS<=_IR;_IS+=1){$P($_.databits,_IS,"0")}for(var _IW=0,_IV=$_.ncws-1;_IW<=_IV;_IW+=1){$_.i=_IW;var _Id=$R($s($_.bpcw),$g($_.cws,$_.i),2);$P($_.databits,$f($_.bpcw-_Id.length+$_.bpcw*$_.i+($_.databits.length-$_.ncws*$_.bpcw)),_Id)}$_.cmv=function(){var _Il=$k[--$j];var _Im=$k[--$j];$k[$j++]=$f(_Im-_Il*$_.size+$_.mid)};$_.lmv=function(){var _Io=$k[--$j];$_.lbit=_Io;var _Ip=$k[--$j];$_.llyr=_Ip;$_.lwid=$f($_.fw+$_.llyr*4);$_.ldir=~~(~~($_.lbit/2)/$_.lwid);if($_.ldir==0){$k[$j++]=$f(-~~(($_.lwid-1)/2)+1+~~($_.lbit/2)%$_.lwid);$k[$j++]=$f(~~(($_.fw-1)/2)+$_.llyr*2+$_.lbit%2);$_.cmv()}if($_.ldir==1){$k[$j++]=$f(~~($_.fw/2)+$_.llyr*2+$_.lbit%2);$k[$j++]=$f(~~(($_.lwid-1)/2)-1-~~($_.lbit/2)%$_.lwid);$_.cmv()}if($_.ldir==2){$k[$j++]=-$f(-~~($_.lwid/2)+1+~~($_.lbit/2)%$_.lwid);$k[$j++]=-$f(~~($_.fw/2)+$_.llyr*2+$_.lbit%2);$_.cmv()}if($_.ldir==3){$k[$j++]=-$f(~~(($_.fw-1)/2)+$_.llyr*2+$_.lbit%2);$k[$j++]=-$f(~~($_.lwid/2)-1-~~($_.lbit/2)%$_.lwid);$_.cmv()}};if($eq($_.format,"full")){$_.fw=12}else{$_.fw=9}$_.size=$f($_.fw+$_.layers*4)+2;$k[$j++]=Infinity;for(var _JR=0,_JS=$_.size*$_.size;_JR<_JS;_JR++){$k[$j++]=-1}$_.pixs=$a();$_.mid=$f(~~(($_.size-1)/2)*$_.size+~~(($_.size-1)/2));$_.i=0;for(var _JY=1,_JX=$_.layers;_JY<=_JX;_JY+=1){$_.layer=_JY;for(var _Jc=0,_Jb=($_.fw+$_.layer*4)*8-1;_Jc<=_Jb;_Jc+=1){$_.pos=_Jc;$k[$j++]=$_.pixs;$k[$j++]=$_.layer;$k[$j++]=$_.pos;$_.lmv();var _Jj=$g($_.databits,$_.databits.length-$_.i-1);var _Jk=$k[--$j];var _Jl=$k[--$j];$p(_Jl,_Jk,_Jj-48);$_.i=$_.i+1}}if($eq($_.format,"full")){$_.fw=13;$_.size=$f($_.fw+$_.layers*4+2+~~(($_.layers+10.5)/7.5-1)*2);$_.mid=~~($_.size*$_.size/2);$k[$j++]=Infinity;for(var _Jv=0,_Jw=$_.size*$_.size;_Jv<_Jw;_Jv++){$k[$j++]=-2}$_.npixs=$a();for(var _Jz=0,_Jy=~~($_.size/2);_Jz<=_Jy;_Jz+=16){$_.i=_Jz;for(var _K2=0,_K1=$_.size-1;_K2<=_K1;_K2+=1){$_.j=_K2;$k[$j++]=$_.npixs;$k[$j++]=-~~($_.size/2)+$_.j;$k[$j++]=$_.i;$_.cmv();var _KB=$k[--$j];var _KC=$k[--$j];$P(_KC,_KB,$a([(~~($_.size/2)+$_.j+$_.i+1)%2]));$k[$j++]=$_.npixs;$k[$j++]=-~~($_.size/2)+$_.j;$k[$j++]=-$_.i;$_.cmv();var _KL=$k[--$j];var _KM=$k[--$j];$P(_KM,_KL,$a([(~~($_.size/2)+$_.j+$_.i+1)%2]));$k[$j++]=$_.npixs;$k[$j++]=$_.i;$k[$j++]=-~~($_.size/2)+$_.j;$_.cmv();var _KV=$k[--$j];var _KW=$k[--$j];$P(_KW,_KV,$a([(~~($_.size/2)+$_.j+$_.i+1)%2]));$k[$j++]=$_.npixs;$k[$j++]=-$_.i;$k[$j++]=-~~($_.size/2)+$_.j;$_.cmv();var _Kf=$k[--$j];var _Kg=$k[--$j];$P(_Kg,_Kf,$a([(~~($_.size/2)+$_.j+$_.i+1)%2]))}}$_.j=0;for(var _Kj=0,_Ki=$_.npixs.length-1;_Kj<=_Ki;_Kj+=1){$_.i=_Kj;if($g($_.npixs,$_.i)==-2){$p($_.npixs,$_.i,$g($_.pixs,$_.j));$_.j=$_.j+1}}$_.pixs=$_.npixs}$_.fw=~~($_.fw/2);for(var _Ky=-$_.fw,_Kx=$_.fw;_Ky<=_Kx;_Ky+=1){$_.i=_Ky;for(var _L2=-$_.fw,_L1=$_.fw;_L2<=_L1;_L2+=1){$_.j=_L2;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.j;$_.cmv();var _LA=$abs($_.i)>$abs($_.j)?$abs($_.i):$abs($_.j);var _LB=$k[--$j];var _LC=$k[--$j];$p(_LC,_LB,(_LA+1)%2)}}var _Ln=$a([$a([-($_.fw+1),$_.fw,1]),$a([-($_.fw+1),$_.fw+1,1]),$a([-$_.fw,$_.fw+1,1]),$a([$_.fw+1,$_.fw+1,1]),$a([$_.fw+1,$_.fw,1]),$a([$_.fw+1,-$_.fw,1]),$a([$_.fw,$_.fw+1,0]),$a([$_.fw+1,-($_.fw+1),0]),$a([$_.fw,-($_.fw+1),0]),$a([-$_.fw,-($_.fw+1),0]),$a([-($_.fw+1),-($_.fw+1),0]),$a([-($_.fw+1),-$_.fw,0])]);for(var _Lo=0,_Lp=_Ln.length;_Lo<_Lp;_Lo++){$k[$j++]=$_.pixs;$q($g(_Ln,_Lo));$r(3,1);$_.cmv();var _Ls=$k[--$j];var _Lt=$k[--$j];var _Lu=$k[--$j];$p(_Lu,_Ls,_Lt)}var _Lw=$eq($_.format,"full")?$_.azteccode_modemapfull:$_.azteccode_modemapcompact;$_.modemap=_Lw;for(var _M0=0,_Lz=$_.modemap.length-1;_M0<=_Lz;_M0+=1){$k[$j++]=_M0;$k[$j++]=$_.pixs;$q($g($_.modemap,_M0));$_.cmv();var _M5=$k[--$j];var _M6=$k[--$j];var _M7=$k[--$j];$p(_M6,_M5,$g($_.modebits,_M7)-48)}$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.size],["pixy",$_.size],["height",$_.size*$_.modunit/72],["width",$_.size*$_.modunit/72],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_azteccodecompact(){var $__=$_;$_=Object.assign({},$_,bwipp_azteccodecompact.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","compact");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_azteccode();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_aztecrune(){var $__=$_;$_=Object.assign({},$_,bwipp_aztecrune.globals);$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$p($_.options,"format","rune");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_azteccode();var _7=$k[--$j];$_.args=_7;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_codeone(){if(!bwipp_codeone.globals){var $__=$_;$_=bwipp_codeone.globals={};$k[$j++]=Infinity;var _0=$a(["T-16","T-32","T-48","S-10","S-20","S-30","A","B","C","D","E","F","G","H"]);for(var _1=0,_2=_0.length;_1<_2;_1++){var _3=$g(_0,_1);$k[$j++]=_3;$k[$j++]=_3}var _4=$d();$_.codeone_versionopts=_4;$_.codeone_stypevals=$a(["1","1010","1100100","1111101000","10011100010000","11000011010100000","11110100001001000000","100110001001011010000000","101111101011110000100000000","111011100110101100101000000000","1001010100000010111110010000000000","1011101001000011101101110100000000000","1110100011010100101001010001000000000000","10010001100001001110011100101010000000000000","10110101111001100010000011110100100000000000000","11100011010111111010100100110001101000000000000000","100011100001101111001001101111110000010000000000000000","101100011010001010111100001011101100010100000000000000000"]);$_.codeone_stypemetrics=$a([$a(["S-10",8,11,10,4,4,1,99,99,99]),$a(["S-20",8,21,20,8,8,1,99,99,99]),$a(["S-30",8,31,30,12,12,1,99,99,99])]);$_.codeone_nonstypemetrics=$a([$a(["A",16,18,16,10,10,1,4,99,6]),$a(["B",22,22,20,19,16,1,4,99,8]),$a(["C",28,32,28,44,26,1,4,22,11]),$a(["D",40,42,36,91,44,1,4,16,16]),$a(["E",52,54,48,182,70,1,4,22,22]),$a(["F",70,76,68,370,140,2,4,22,31]),$a(["G",104,98,88,732,280,4,6,21,47]),$a(["H",148,134,120,1480,560,8,6,20,69]),$a(["T-16",16,17,16,10,10,1,99,99,99]),$a(["T-32",16,33,32,24,16,1,99,99,99]),$a(["T-48",16,49,48,38,22,1,99,99,99])]);$_.codeone_fnc1=-1;$_.codeone_fnc3=-2;$_.codeone_lc=-5;$_.codeone_lb=-6;$_.codeone_lx=-7;$_.codeone_lt=-8;$_.codeone_ld=-9;$_.codeone_unl=-10;$_.codeone_fnc2=-11;$_.codeone_fnc4=-12;$_.codeone_sft1=-13;$_.codeone_sft2=-14;$_.codeone_sft3=-15;$_.codeone_eci=-16;$_.codeone_pad=-17;$_.codeone_fnc1ld=-18;$_.codeone_unlcw=255;$_.codeone_a=0;$_.codeone_c=1;$_.codeone_t=2;$_.codeone_x=3;$_.codeone_d=4;$_.codeone_b=5;$_.codeone_encfuncs=$a(["encA","encCTX","encCTX","encCTX","encD","encB"]);var _N=new Map([["A","121343"],["B","12134343"],["C","12121343"],["D","1213434343"],["E","1212134343"],["F","1212121343"],["G","121213434343"],["H","121212134343"],["S","56661278"],["T","5666666666127878"]]);$_.codeone_cpatmap=_N;var _11=new Map([["A",$a([$a([12,5])])],["B",$a([$a([16,7])])],["C",$a([$a([26,12])])],["D",$a([])],["E",$a([$a([26,23])])],["F",$a([$a([26,32]),$a([70,32]),$a([26,34]),$a([70,34])])],["G",$a([$a([27,48]),$a([69,48])])],["H",$a([$a([26,70]),$a([66,70]),$a([106,70]),$a([26,72]),$a([66,72]),$a([106,72])])],["S-10",$a([])],["S-20",$a([$a([10,4])])],["S-30",$a([$a([15,4]),$a([15,6])])],["T-16",$a([$a([8,10])])],["T-32",$a([$a([16,10]),$a([16,12])])],["T-48",$a([$a([24,10]),$a([24,12]),$a([24,14])])]]);$_.codeone_blackdotmap=_11;$_.codeone_rsparams=$a([$a([]),$a([]),$a([]),$a([]),$a([]),$a([32,37]),$a([]),$a([]),$a([256,301])]);$k[$j++]=Infinity;for(var _1C=0;_1C<=128;_1C+=1){$k[$j++]=_1C;$k[$j++]=_1C+1}$k[$j++]=$_.codeone_pad;$k[$j++]=129;for(var _1D=0;_1D<=99;_1D+=1){var _1F=$R($s(2),_1D,10);var _1H=$Z($s(2),"00");$P(_1H,2-_1F.length,_1F);$k[$j++]=_1H;$k[$j++]=_1D+130}var _1I=$a([$_.codeone_lc,$_.codeone_lb,$_.codeone_fnc1,$_.codeone_fnc2,$_.codeone_fnc3,$_.codeone_fnc4,$_.codeone_fnc1ld]);$k[$j++]=229;for(var _1J=0,_1K=_1I.length;_1J<_1K;_1J++){var _1M=$k[--$j];var _1N=_1M+1;$k[$j++]=$g(_1I,_1J);$k[$j++]=_1N;$k[$j++]=_1N}$k[$j-1]=$_.codeone_lx;$k[$j++]=238;$k[$j++]=$_.codeone_lt;$k[$j++]=239;$_.avals=$d();$k[$j++]=Infinity;var _1O=$_.avals;for(var _1T=_1O.size,_1S=_1O.keys(),_1R=0;_1R<_1T;_1R++){var _1P=_1S.next().value;var _1Q=_1O.get(_1P);$k[$j++]=_1P;$k[$j++]=Infinity;$k[$j++]=_1Q;var _1W=$a();$k[$j++]=_1W}$_.avals=$d();$k[$j++]=Infinity;$k[$j++]=$_.codeone_sft1;$k[$j++]=0;$k[$j++]=$_.codeone_sft2;$k[$j++]=1;$k[$j++]=$_.codeone_sft3;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _1X=48;_1X<=57;_1X+=1){$k[$j++]=_1X;$k[$j++]=_1X-44}for(var _1Y=65;_1Y<=90;_1Y+=1){$k[$j++]=_1Y;$k[$j++]=_1Y-51}var _1Z=$d();$_.cnvals=_1Z;$k[$j++]=Infinity;for(var _1a=0;_1a<=31;_1a+=1){$k[$j++]=_1a;$k[$j++]=_1a}var _1b=$d();$_.c1vals=_1b;$k[$j++]=Infinity;for(var _1c=33;_1c<=47;_1c+=1){$k[$j++]=_1c;$k[$j++]=_1c-33}for(var _1d=58;_1d<=64;_1d+=1){$k[$j++]=_1d;$k[$j++]=_1d-43}for(var _1e=91;_1e<=95;_1e+=1){$k[$j++]=_1e;$k[$j++]=_1e-69}$k[$j++]=$_.codeone_fnc1;$k[$j++]=27;$k[$j++]=$_.codeone_fnc2;$k[$j++]=28;$k[$j++]=$_.codeone_fnc3;$k[$j++]=29;$k[$j++]=$_.codeone_fnc4;$k[$j++]=30;$k[$j++]=$_.codeone_pad;$k[$j++]=31;var _1f=$d();$_.c2vals=_1f;$k[$j++]=Infinity;for(var _1g=96;_1g<=127;_1g+=1){$k[$j++]=_1g;$k[$j++]=_1g-96}var _1h=$d();$_.c3vals=_1h;$k[$j++]=Infinity;var _1i=$_.cnvals;for(var _1n=_1i.size,_1m=_1i.keys(),_1l=0;_1l<_1n;_1l++){var _1j=_1m.next().value;var _1k=_1i.get(_1j);$k[$j++]=_1j;$k[$j++]=Infinity;$k[$j++]=_1k;var _1q=$a();$k[$j++]=_1q}var _1r=$_.c1vals;for(var _1w=_1r.size,_1v=_1r.keys(),_1u=0;_1u<_1w;_1u++){var _1s=_1v.next().value;var _1t=_1r.get(_1s);$k[$j++]=_1s;$k[$j++]=Infinity;$k[$j++]=$g($_.cnvals,$_.codeone_sft1);$k[$j++]=_1t;var _21=$a();$k[$j++]=_21}var _22=$_.c2vals;for(var _27=_22.size,_26=_22.keys(),_25=0;_25<_27;_25++){var _23=_26.next().value;var _24=_22.get(_23);$k[$j++]=_23;$k[$j++]=Infinity;$k[$j++]=$g($_.cnvals,$_.codeone_sft2);$k[$j++]=_24;var _2C=$a();$k[$j++]=_2C}var _2D=$_.c3vals;for(var _2I=_2D.size,_2H=_2D.keys(),_2G=0;_2G<_2I;_2G++){var _2E=_2H.next().value;var _2F=_2D.get(_2E);$k[$j++]=_2E;$k[$j++]=Infinity;$k[$j++]=$g($_.cnvals,$_.codeone_sft3);$k[$j++]=_2F;var _2N=$a();$k[$j++]=_2N}$_.cvals=$d();$k[$j++]=Infinity;$k[$j++]=$_.codeone_sft1;$k[$j++]=0;$k[$j++]=$_.codeone_sft2;$k[$j++]=1;$k[$j++]=$_.codeone_sft3;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _2O=48;_2O<=57;_2O+=1){$k[$j++]=_2O;$k[$j++]=_2O-44}for(var _2P=97;_2P<=122;_2P+=1){$k[$j++]=_2P;$k[$j++]=_2P-83}var _2Q=$d();$_.tnvals=_2Q;$k[$j++]=Infinity;for(var _2R=0;_2R<=31;_2R+=1){$k[$j++]=_2R;$k[$j++]=_2R}var _2S=$d();$_.t1vals=_2S;$k[$j++]=Infinity;for(var _2T=33;_2T<=47;_2T+=1){$k[$j++]=_2T;$k[$j++]=_2T-33}for(var _2U=58;_2U<=64;_2U+=1){$k[$j++]=_2U;$k[$j++]=_2U-43}for(var _2V=91;_2V<=95;_2V+=1){$k[$j++]=_2V;$k[$j++]=_2V-69}$k[$j++]=$_.codeone_fnc1;$k[$j++]=27;$k[$j++]=$_.codeone_fnc2;$k[$j++]=28;$k[$j++]=$_.codeone_fnc3;$k[$j++]=29;$k[$j++]=$_.codeone_fnc4;$k[$j++]=30;$k[$j++]=$_.codeone_pad;$k[$j++]=31;var _2W=$d();$_.t2vals=_2W;$k[$j++]=Infinity;$k[$j++]=96;$k[$j++]=0;for(var _2X=65;_2X<=90;_2X+=1){$k[$j++]=_2X;$k[$j++]=_2X-64}for(var _2Y=123;_2Y<=127;_2Y+=1){$k[$j++]=_2Y;$k[$j++]=_2Y-96}var _2Z=$d();$_.t3vals=_2Z;$k[$j++]=Infinity;var _2a=$_.tnvals;for(var _2f=_2a.size,_2e=_2a.keys(),_2d=0;_2d<_2f;_2d++){var _2b=_2e.next().value;var _2c=_2a.get(_2b);$k[$j++]=_2b;$k[$j++]=Infinity;$k[$j++]=_2c;var _2i=$a();$k[$j++]=_2i}var _2j=$_.t1vals;for(var _2o=_2j.size,_2n=_2j.keys(),_2m=0;_2m<_2o;_2m++){var _2k=_2n.next().value;var _2l=_2j.get(_2k);$k[$j++]=_2k;$k[$j++]=Infinity;$k[$j++]=$g($_.tnvals,$_.codeone_sft1);$k[$j++]=_2l;var _2t=$a();$k[$j++]=_2t}var _2u=$_.t2vals;for(var _2z=_2u.size,_2y=_2u.keys(),_2x=0;_2x<_2z;_2x++){var _2v=_2y.next().value;var _2w=_2u.get(_2v);$k[$j++]=_2v;$k[$j++]=Infinity;$k[$j++]=$g($_.tnvals,$_.codeone_sft2);$k[$j++]=_2w;var _34=$a();$k[$j++]=_34}var _35=$_.t3vals;for(var _3A=_35.size,_39=_35.keys(),_38=0;_38<_3A;_38++){var _36=_39.next().value;var _37=_35.get(_36);$k[$j++]=_36;$k[$j++]=Infinity;$k[$j++]=$g($_.tnvals,$_.codeone_sft3);$k[$j++]=_37;var _3F=$a();$k[$j++]=_3F}$_.tvals=$d();for(var _3G=128;_3G<=255;_3G+=1){$_.i=_3G;var _3Q=$_.avals;var _3P=$_.i;$k[$j++]=Infinity;$q($g($_.avals,$_.codeone_fnc4));$q($g($_.avals,$_.i-128));var _3O=$a();$p(_3Q,_3P,_3O);var _3a=$_.cvals;var _3Z=$_.i;$k[$j++]=Infinity;$q($g($_.cvals,$_.codeone_fnc4));$q($g($_.cvals,$_.i-128));var _3Y=$a();$p(_3a,_3Z,_3Y);var _3k=$_.tvals;var _3j=$_.i;$k[$j++]=Infinity;$q($g($_.tvals,$_.codeone_fnc4));$q($g($_.tvals,$_.i-128));var _3i=$a();$p(_3k,_3j,_3i)}$_.avals=$_.avals;$_.cvals=$_.cvals;$_.tvals=$_.tvals;$k[$j++]=Infinity;$k[$j++]=13;$k[$j++]=0;$k[$j++]=42;$k[$j++]=1;$k[$j++]=62;$k[$j++]=2;$k[$j++]=32;$k[$j++]=3;for(var _3o=48;_3o<=57;_3o+=1){$k[$j++]=_3o;$k[$j++]=_3o-44}for(var _3p=65;_3p<=90;_3p+=1){$k[$j++]=_3p;$k[$j++]=_3p-51}var _3q=$d();$_.xvals=_3q;$k[$j++]=Infinity;var _3r=$_.xvals;for(var _3w=_3r.size,_3v=_3r.keys(),_3u=0;_3u<_3w;_3u++){var _3s=_3v.next().value;var _3t=_3r.get(_3s);$k[$j++]=_3s;$k[$j++]=Infinity;$k[$j++]=_3t;var _3z=$a();$k[$j++]=_3z}var _40=$d();$_.xvals=_40;$k[$j++]=Infinity;for(var _41=0;_41<=255;_41+=1){$k[$j++]=_41;$k[$j++]=_41}var _42=$d();$_.bvals=_42;$k[$j++]=Infinity;var _43=$_.bvals;for(var _48=_43.size,_47=_43.keys(),_46=0;_46<_48;_46++){var _44=_47.next().value;var _45=_43.get(_44);$k[$j++]=_44;$k[$j++]=Infinity;$k[$j++]=_45;var _4B=$a();$k[$j++]=_4B}var _4C=$d();$_.bvals=_4C;$_.encvals=$a([$_.avals,$_.cvals,$_.tvals,$_.xvals,-1,$_.bvals]);$k[$j++]=Infinity;for(var _4J=0,_4K=$_.codeone_rsparams.length;_4J<_4K;_4J++){var _4L=$g($_.codeone_rsparams,_4J);$k[$j++]=_4L;if(_4L.length!=0){var _4M=$k[--$j];$q(_4M);var _4N=$k[--$j];$_.rspm=_4N;var _4O=$k[--$j];$_.rsgf=_4O;$k[$j++]=Infinity;$k[$j++]=1;for(var _4Q=0,_4R=$_.rsgf-1;_4Q<_4R;_4Q++){var _4S=$k[$j-1];var _4T=_4S*2;$k[$j++]=_4T;if(_4T>=$_.rsgf){var _4W=$k[--$j];$k[$j++]=$xo(_4W,$_.rspm)}}$_.rsalog=$a();$_.rslog=$a($_.rsgf);for(var _4b=1,_4a=$_.rsgf-1;_4b<=_4a;_4b+=1){$p($_.rslog,$g($_.rsalog,_4b),_4b)}$k[$j++]=$a([$_.rslog,$_.rsalog])}else{$k[$j-1]=$a([])}}var _4j=$a();$_.rstables=_4j;$_.codeone_rsprod=function(){var _4k=$k[$j-1];if(_4k!=0){var _4m=$k[$j-2];if(_4m!=0){var _4o=$k[--$j];var _4q=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_4o)+$g($_.rslog,_4q))%$_.gf_1)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.codeone_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _4x=0,_4y=$_.ecpb;_4x<_4y;_4x++){$k[$j++]=0}$_.coeffs=$a();for(var _51=0,_50=$_.ecpb-1;_51<=_50;_51+=1){var _54=$_.coeffs;$p(_54,_51+1,$g(_54,_51+1-1));$k[$j++]=$g($_.rsalog,_51);for(var _56=_51;_56>=1;_56-=1){var _57=$_.coeffs;var _59=$k[$j-1];$k[$j++]=_57;$k[$j++]=_56;$k[$j++]=$g(_57,_56);$k[$j++]=_59;$_.codeone_rsprod();var _5B=$k[--$j];var _5C=$k[--$j];var _5E=$k[--$j];$p(_5E,_5C,$xo(_5B,$g($_.coeffs,_5C-1)))}var _5F=$_.coeffs;var _5H=$k[$j-1];$k[$j++]=_5F;$k[$j++]=0;$k[$j++]=$g(_5F,0);$k[$j++]=_5H;$_.codeone_rsprod();var _5I=$k[--$j];var _5J=$k[--$j];var _5K=$k[--$j];$p(_5K,_5J,_5I);$j--}$k[$j++]=$G($_.coeffs,0,$_.coeffs.length-1)};$_.codeone_coeffscachemax=9;$_.codeone_coeffscachelimit=212;$k[$j++]=$_.codeone_coeffscachemax;$k[$j++]=$_.codeone_coeffscachelimit;bwipp_fifocache();var _5Q=$k[--$j];$_.codeone_coeffscache=_5Q;$_.codeone_coeffscachefetch=function(){var _5R=$k[--$j];$_.rsbt=_5R;var _5S=$k[--$j];$_.ecpb=_5S;$_.rslog=$g($g($_.rstables,$_.rsbt),0);$_.rsalog=$g($g($_.rstables,$_.rsbt),1);$_.gf=$_.rsalog.length;$_.gf_1=$_.gf-1;var _5f=$g($_.codeone_coeffscache,"fetch");$k[$j++]=$f($_.rsbt*1e4+$_.ecpb);$k[$j++]=function(){$_.codeone_gencoeffs()};$k[$j++]=function(){var _5g=$k[--$j];$k[$j++]=_5g.length};if(_5f()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_codeone.globals);$_.dontdraw=false;$_.version="unset";$_.parse=false;$_.parsefnc=false;bwipp_processoptions();var _5h=$k[--$j];$_.options=_5h;var _5i=$k[--$j];$_.barcode=_5i;if($_.barcode.length>4e3){$k[$j++]="bwipp.codeoneinputTooLong#31085";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($ne($_.version,"unset")){if(!$has($_.codeone_versionopts,$_.version)){$k[$j++]="bwipp.codeoneBadVersion#31112";$k[$j++]="version must be A to H, T-16, T-32, T-48, S-10, S-20 or S-30";bwipp_raiseerror()}}$_.stype=$eq($G($_.version,0,1),"S");$_.ttype=$eq($G($_.version,0,1),"T");if($_.stype){$F($_.barcode,function(){var _5v=$k[--$j];if(_5v<48||_5v>57){$k[$j++]="bwipp.codeoneStypeNonDigit#31127";$k[$j++]="S-Type symbols can only contain digits";bwipp_raiseerror()}});$_.barlen=$_.barcode.length;if($_.barlen>18){$k[$j++]="bwipp.codeoneStypeTooLong#31133";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$_.normalize=function(){var _5y=$k[--$j];$_.base=_5y;var _5z=$k[--$j];$_.num=_5z;for(var _61=$_.num.length-1;_61>=1;_61-=1){$_.i=_61;var _62=$_.num;var _63=$_.i;$p(_62,_63-1,$f($g(_62,_63-1)+~~($g($_.num,$_.i)/$_.base)));$p($_.num,$_.i,$g($_.num,$_.i)%$_.base)}if($ge($g($_.num,0),$_.base)){$k[$j++]=Infinity;$k[$j++]=$g($_.num,0);for(;;){var _6K=$k[$j-1];if($lt(_6K,$_.base)){break}var _6M=$k[--$j];$k[$j++]=_6M%$_.base;$k[$j++]=~~(_6M/$_.base)}$_.extcnt=$m()-1;$as($a($m()));var _6S=$k[--$j];$_.extdigits=_6S;$k[$j-1]=Infinity;for(var _6U=$_.extcnt;_6U>=0;_6U-=1){$k[$j++]=$g($_.extdigits,_6U)}$q($G($_.num,1,$_.num.length-1));$_.num=$a()}$k[$j++]=0;for(;;){var _6a=$k[$j-1];if(_6a>=$_.num.length){break}var _6d=$k[$j-1];if($g($_.num,_6d)!=0){break}$k[$j-1]+=1}var _6g=$k[$j-1];if(_6g>=$_.num.length){$k[$j-1]=$a([0])}else{var _6k=$k[--$j];$k[$j++]=$G($_.num,_6k,$f($_.num.length-_6k))}};$_.bigadd=function(){var _6n=$k[--$j];var _6o=$k[--$j];$_.offset=$abs(_6n.length-_6o.length);if(_6n.length<_6o.length){var _=_6n;_6n=_6o;_6o=_}$_.a=_6n;$_.b=_6o;for(var _6r=0,_6q=$_.b.length-1;_6r<=_6q;_6r+=1){var _6s=$_.a;var _6t=$_.offset;$p(_6s,_6r+_6t,$f($g(_6s,_6r+_6t)+$g($_.b,_6r)))}$k[$j++]=$_.a};$_.barlen=$_.barcode.length;$_.v=$a([1]);for(var _71=0,_70=$_.barlen-1;_71<=_70;_71+=1){$_.i=_71;$k[$j++]=Infinity;$F($g($_.codeone_stypevals,$_.i),function(){var _74=$k[--$j];$k[$j++]=(_74-48)*($g($_.barcode,$_.barlen-$_.i-1)-48)});var _79=$a();$k[$j++]=_79;$k[$j++]=$_.v;$_.bigadd();var _7B=$k[--$j];$_.v=_7B}$k[$j++]="v";$k[$j++]=$_.v;$k[$j++]=2;$_.normalize();var _7D=$k[--$j];var _7E=$k[--$j];$_[_7E]=_7D;$k[$j++]=Infinity;for(var _7G=0,_7H=(5-$_.v.length%5)%5;_7G<_7H;_7G++){$k[$j++]=0}$q($_.v);$_.v=$a();$_.cws=$a(~~($_.v.length/5));for(var _7N=0,_7M=$_.cws.length-1;_7N<=_7M;_7N+=1){$_.i=_7N;var _7Q=$G($_.v,$_.i*5,5);var _7V=0;for(var _7R=0,_7S=_7Q.length;_7R<_7S;_7R++){_7V=$f(_7V+$g(_7Q,_7R))*2}$p($_.cws,$_.i,~~(_7V/2))}$_.metrics=$_.codeone_stypemetrics}else{$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true],["FNC1",$_.codeone_fnc1],["FNC3",$_.codeone_fnc3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _7c=$k[--$j];$_.msg=_7c;$_.msglen=$_.msg.length;$_.eciesc=92;$k[$j++]="numecis";$k[$j++]=0;$F($_.msg,function(){var _7f=$k[--$j];if(_7f<=-1e6){$k[$j-1]+=1}});var _7h=$k[--$j];var _7i=$k[--$j];$_[_7i]=_7h;if($_.numecis>0){$_.msgtmp=$a($f($_.msg.length*2+$_.numecis*6)+2);$p($_.msgtmp,0,$_.codeone_pad);$p($_.msgtmp,1,$_.eciesc);$_.j=2;for(var _7s=0,_7r=$_.msg.length-1;_7s<=_7r;_7s+=1){var _7u=$g($_.msg,_7s);$k[$j++]=_7u;if(_7u<=-1e6){var _7v=$k[--$j];var _7y=$G($R($s(7),-_7v,10),1,6);for(var _7z=0,_80=_7y.length;_7z<_80;_7z++){$k[$j++]=$g(_7y,_7z)}$as($a(6));var _84=$k[--$j];$P($_.msgtmp,$_.j+1,_84);$p($_.msgtmp,$_.j,$_.eciesc);$_.j=$_.j+7}else{var _8A=$k[$j-1];if(_8A!=$_.eciesc){var _8D=$k[--$j];$p($_.msgtmp,$_.j,_8D);$_.j=$_.j+1}else{$j--;$p($_.msgtmp,$_.j,$_.eciesc);$p($_.msgtmp,$_.j+1,$_.eciesc);$_.j=$_.j+2}}}$_.msg=$G($_.msgtmp,0,$_.j);$_.msglen=$_.msg.length}$_.metrics=$_.codeone_nonstypemetrics;$k[$j++]=Infinity;var _8R=$_.metrics;for(var _8S=0,_8T=_8R.length;_8S<_8T;_8S++){$_.m=$g(_8R,_8S);$_.vers=$g($_.m,0);$_.dcws=$g($_.m,4);$_.okay=true;if($eq($_.version,"unset")){if($_.vers.length!=1){$_.okay=false}}else{if($ne($_.version,$_.vers)){$_.okay=false}}if($_.okay){$k[$j++]=$_.dcws}}$as($a($m()));var _8h=$k[--$j];$_.fullcws=_8h;$k[$j-1]=Infinity;for(var _8i=0;_8i<1480;_8i++){$k[$j++]=1e4}$_.numremcws=$a();$F($_.fullcws,function(){var _8l=$k[--$j];$p($_.numremcws,_8l-1,1)});for(var _8m=1478;_8m>=0;_8m-=1){$_.i=_8m;if($g($_.numremcws,$_.i)!=1){$p($_.numremcws,$_.i,$g($_.numremcws,$_.i+1)+1)}}$_.getnumremcws=function(){var _8v=$k[$j-1];if(_8v>=$_.numremcws.length){$k[$j-1]="bwipp.codeoneTooLong#31309";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}var _8y=$k[--$j];$k[$j++]=$g($_.numremcws,_8y)};$k[$j++]=Infinity;for(var _91=0,_92=$_.msglen;_91<_92;_91++){$k[$j++]=0}$k[$j++]=0;$_.numD=$a();$k[$j++]=Infinity;for(var _94=0,_95=$_.msglen;_94<_95;_94++){$k[$j++]=0}$k[$j++]=9999;$_.nextXterm=$a();$k[$j++]=Infinity;for(var _97=0,_98=$_.msglen;_97<_98;_97++){$k[$j++]=0}$k[$j++]=9999;$_.nextNonX=$a();for(var _9A=$_.msglen-1;_9A>=0;_9A-=1){$_.i=_9A;$_.barchar=$g($_.msg,$_.i);if($_.barchar>=48&&$_.barchar<=57){$p($_.numD,$_.i,$g($_.numD,$_.i+1)+1)}if($_.barchar==13||$_.barchar==42||$_.barchar==62){$p($_.nextXterm,$_.i,0)}else{$p($_.nextXterm,$_.i,$g($_.nextXterm,$_.i+1)+1)}if(!$has($_.xvals,$_.barchar)){$p($_.nextNonX,$_.i,0)}else{$p($_.nextNonX,$_.i,$g($_.nextNonX,$_.i+1)+1)}}$k[$j++]=Infinity;var _9f=$_.nextXterm;for(var _9g=0,_9h=_9f.length;_9g<_9h;_9g++){var _9i=$g(_9f,_9g);$k[$j++]=_9i;if(_9i>1e4){$k[$j-1]=1e4}}$_.nextXterm=$a();$k[$j++]=Infinity;var _9j=$_.nextNonX;for(var _9k=0,_9l=_9j.length;_9k<_9l;_9k++){var _9m=$g(_9j,_9k);$k[$j++]=_9m;if(_9m>1e4){$k[$j-1]=1e4}}$_.nextNonX=$a();$_.isD=function(){$k[$j++]=$_.char>=48&&$_.char<=57};$_.isC=function(){$k[$j++]=$has($_.cnvals,$_.char)};$_.isT=function(){$k[$j++]=$has($_.tnvals,$_.char)};$_.isX=function(){$k[$j++]=$has($_.xvals,$_.char)};$_.isEA=function(){$k[$j++]=$_.char>127};$_.isFN=function(){$k[$j++]=$_.char<0};$_.XtermFirst=function(){var _A0=$k[--$j];$k[$j++]=$lt($g($_.nextXterm,_A0),$g($_.nextNonX,_A0))};$_.lookup=function(){$_.ac=1;$_.cc=2;$_.tc=2;$_.xc=2;$_.bc=3;if($_.mode==$_.codeone_a){$_.ac=0;$_.cc=1;$_.tc=1;$_.xc=1;$_.bc=2}if($_.mode==$_.codeone_c){$_.cc=0}if($_.mode==$_.codeone_t){$_.tc=0}if($_.mode==$_.codeone_x){$_.xc=0}if($_.mode==$_.codeone_b){$_.bc=0}for(var _AA=0;_AA<1;_AA++){$_.k=0;for(;;){if($_.i+$_.k==$_.msglen){var _AI=$a([$_.ac,$_.cc,$_.tc,$_.xc]);var _AO=true;for(var _AJ=0,_AK=_AI.length;_AJ<_AK;_AJ++){_AO=_AO&&$_.bc<=$ceil($g(_AI,_AJ))}if(_AO){$k[$j++]=$_.codeone_b;break}var _AT=$a([$_.cc,$_.tc,$_.xc,$_.bc]);var _AZ=true;for(var _AU=0,_AV=_AT.length;_AU<_AV;_AU++){_AZ=_AZ&&$_.ac<=$ceil($g(_AT,_AU))}if(_AZ){$k[$j++]=$_.codeone_a;break}var _Ac=$a([$_.tc,$_.xc]);var _Ai=true;for(var _Ad=0,_Ae=_Ac.length;_Ad<_Ae;_Ad++){_Ai=_Ai&&$ceil($_.cc)<=$ceil($g(_Ac,_Ad))}if(_Ai){$k[$j++]=$_.codeone_c;break}var _Ak=$a([$_.xc]);var _Aq=true;for(var _Al=0,_Am=_Ak.length;_Al<_Am;_Al++){_Aq=_Aq&&$ceil($_.tc)<=$ceil($g(_Ak,_Al))}if(_Aq){$k[$j++]=$_.codeone_t;break}$k[$j++]=$_.codeone_x;break}$_.char=$g($_.msg,$_.i+$_.k);$k[$j++]="ac";$k[$j++]=$_.ac;$_.isD();var _Aw=$k[--$j];if(_Aw){var _Ax=$k[--$j];$k[$j++]=$f(_Ax+1/2)}else{$_.isEA();var _Ay=$k[--$j];if(_Ay){var _Az=$k[--$j];$k[$j++]=$ceil(_Az)+2}else{var _B0=$k[--$j];$k[$j++]=$ceil(_B0)+1}}var _B1=$k[--$j];var _B2=$k[--$j];$_[_B2]=_B1;$k[$j++]="cc";$k[$j++]=$_.cc;$_.isC();var _B4=$k[--$j];if(_B4){var _B5=$k[--$j];$k[$j++]=$f(_B5+.6666667)}else{$_.isEA();var _B6=$k[--$j];if(_B6){var _B7=$k[--$j];$k[$j++]=$f(_B7+2.6666667)}else{var _B8=$k[--$j];$k[$j++]=$f(_B8+1.3333334)}}var _B9=$k[--$j];var _BA=$k[--$j];$_[_BA]=_B9;$k[$j++]="tc";$k[$j++]=$_.tc;$_.isT();var _BC=$k[--$j];if(_BC){var _BD=$k[--$j];$k[$j++]=$f(_BD+.6666667)}else{$_.isEA();var _BE=$k[--$j];if(_BE){var _BF=$k[--$j];$k[$j++]=$f(_BF+2.6666667)}else{var _BG=$k[--$j];$k[$j++]=$f(_BG+1.3333334)}}var _BH=$k[--$j];var _BI=$k[--$j];$_[_BI]=_BH;$k[$j++]="xc";$k[$j++]=$_.xc;$_.isX();var _BK=$k[--$j];if(_BK){var _BL=$k[--$j];$k[$j++]=$f(_BL+.6666667)}else{$_.isEA();var _BM=$k[--$j];if(_BM){var _BN=$k[--$j];$k[$j++]=$f(_BN+4.3333334)}else{var _BO=$k[--$j];$k[$j++]=$f(_BO+3.3333334)}}var _BP=$k[--$j];var _BQ=$k[--$j];$_[_BQ]=_BP;$k[$j++]="bc";$k[$j++]=$_.bc;$_.isFN();var _BS=$k[--$j];if(_BS){$k[$j-1]+=3}else{$k[$j-1]+=1}var _BV=$k[--$j];var _BW=$k[--$j];$_[_BW]=_BV;if($_.k>=3){var _Bc=$a([$_.ac,$_.cc,$_.tc,$_.xc]);var _Bi=true;for(var _Bd=0,_Be=_Bc.length;_Bd<_Be;_Bd++){_Bi=_Bi&&$_.bc+1<=$ceil($g(_Bc,_Bd))}if(_Bi){$k[$j++]=$_.codeone_b;break}var _Bn=$a([$_.cc,$_.tc,$_.xc,$_.bc]);var _Bt=true;for(var _Bo=0,_Bp=_Bn.length;_Bo<_Bp;_Bo++){_Bt=_Bt&&$_.ac+1<=$ceil($g(_Bn,_Bo))}if(_Bt){$k[$j++]=$_.codeone_a;break}var _By=$a([$_.ac,$_.cc,$_.xc,$_.bc]);var _C4=true;for(var _Bz=0,_C0=_By.length;_Bz<_C0;_Bz++){_C4=_C4&&$ceil($_.tc)+1<=$ceil($g(_By,_Bz))}if(_C4){$k[$j++]=$_.codeone_t;break}var _C7=$a([$_.ac,$_.tc]);var _CD=true;for(var _C8=0,_C9=_C7.length;_C8<_C9;_C8++){_CD=_CD&&$ceil($_.cc)+1<=$ceil($g(_C7,_C8))}if(_CD){if($ceil($_.cc)<$ceil($_.xc)){$k[$j++]=$_.codeone_c;break}if($_.cc==$_.xc){$k[$j++]=$_.i+$_.k+1;$_.XtermFirst();var _CK=$k[--$j];if(_CK){$k[$j++]=$_.codeone_x;break}else{$k[$j++]=$_.codeone_c;break}}}var _CP=$a([$_.ac,$_.cc,$_.tc,$_.bc]);var _CV=true;for(var _CQ=0,_CR=_CP.length;_CQ<_CR;_CQ++){_CV=_CV&&$ceil($_.xc)+1<=$ceil($g(_CP,_CQ))}if(_CV){$k[$j++]=$_.codeone_x;break}}$_.k=$_.k+1}}};$_.addtocws=function(){var _CX=$k[$j-1];if(_CX.length+$_.j>1480){$k[$j-1]="bwipp.codeoneTooLong#31384";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}var _CZ=$k[--$j];$P($_.cws,$_.j,_CZ);$_.j=_CZ.length+$_.j};$_.tobin=function(){var _Cd=$k[--$j];var _Ce=$s(_Cd);$k[$j++]=_Ce;for(var _Cg=0,_Cf=_Ce.length-1;_Cg<=_Cf;_Cg+=1){var _Ch=$k[$j-1];$p(_Ch,_Cg,48)}var _Ci=$k[$j-1];$k[$j++]=_Ci;var _Cj=$k[$j-3];var _Ck=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _Cm=$R($s(_Ck.length),_Cj,2);$P(_Ck,_Ck.length-_Cm.length,_Cm);var _Co=$k[--$j];$k[$j++]=Infinity;$F(_Co,function(){$k[$j-1]-=48});var _Cq=$a();$k[$j++]=_Cq};$_.Dbitsbuf=$a(~~($_.msglen*10/3)+20);$_.Dbitslen=0;$_.appendbits=function(){var _Ct=$k[--$j];$_.apnd_n=_Ct;$as($a($_.apnd_n));$k[$j++]=$_.Dbitsbuf;$k[$j++]=$_.Dbitslen;var _Cy=$k[$j-3];var _Cz=$k[$j-1];var _D0=$k[$j-2];$j-=3;$P(_D0,_Cz,_Cy);$_.Dbitslen=$f($_.Dbitslen+$_.apnd_n)};$_.encA=function(){for(var _D3=0;_D3<1;_D3++){if($g($_.numD,$_.i)>=21){$p($_.Dbitsbuf,0,1);$p($_.Dbitsbuf,1,1);$p($_.Dbitsbuf,2,1);$p($_.Dbitsbuf,3,1);$_.Dbitslen=4;$_.mode=$_.codeone_d;break}var _DD=$g($_.numD,$_.i);if(_DD>=13&&$f(_DD+$_.i)==$_.msglen){$p($_.Dbitsbuf,0,1);$p($_.Dbitsbuf,1,1);$p($_.Dbitsbuf,2,1);$p($_.Dbitsbuf,3,1);$_.Dbitslen=4;$_.mode=$_.codeone_d;break}if($g($_.numD,$_.i)>=2){var _DN=$s(2);$p(_DN,0,$g($_.msg,$_.i));$p(_DN,1,$g($_.msg,$_.i+1));$k[$j++]=$g($_.avals,_DN);$_.addtocws();$_.i=$_.i+2;break}if($g($_.msg,$_.i)==$_.codeone_fnc1){if($g($_.numD,$_.i+1)>=15){$k[$j++]=$g($_.avals,$_.codeone_fnc1ld);$_.addtocws();$_.i=$_.i+1;$_.Dbitslen=0;$_.mode=$_.codeone_d;break}var _Di=$g($_.numD,$_.i+1);if(_Di>=7&&$f(_Di+$_.i)+1==$_.msglen){$k[$j++]=$g($_.avals,$_.codeone_fnc1ld);$_.addtocws();$_.i=$_.i+1;$_.Dbitslen=0;$_.mode=$_.codeone_d;break}}$k[$j++]="newmode";$_.lookup();var _Do=$k[--$j];var _Dp=$k[--$j];$_[_Dp]=_Do;if($_.newmode!=$_.mode){$k[$j++]=$g($_.avals,$g($a([-1,$_.codeone_lc,$_.codeone_lt,$_.codeone_lx,$_.codeone_ld,$_.codeone_lb]),$_.newmode));$_.addtocws();$_.mode=$_.newmode;break}$k[$j++]=$g($_.avals,$g($_.msg,$_.i));$_.addtocws();$_.i=$_.i+1;break}};$_.CTXvalstocws=function(){var _E4=$k[--$j];$_.in=_E4;$k[$j++]=Infinity;for(var _E7=0,_E6=$_.in.length-1;_E7<=_E6;_E7+=3){var _E9=$G($_.in,_E7,3);var _EE=0;for(var _EA=0,_EB=_E9.length;_EA<_EB;_EA++){_EE=$f(_EE+$g(_E9,_EA))*40}var _EF=~~(_EE/40)+1;$k[$j++]=~~(_EF/256);$k[$j++]=_EF%256}$as($a($m()));var _EI=$k[--$j];$k[$j-1]=_EI};$_.encCTX=function(){$_.p=0;$_.ctxvals=$a(2220);for(;;){if($_.i==$_.msglen){break}if($_.p%3==0){if($g($_.numD,$_.i)>=12){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws();$_.mode=$_.codeone_a;break}var _EX=$g($_.numD,$_.i);if(_EX>=8&&$f(_EX+$_.i)==$_.msglen){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws();$_.mode=$_.codeone_a;break}if($_.mode==$_.codeone_x){if(!$has($_.xvals,$g($_.msg,$_.i))){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$_.j;$_.getnumremcws();var _Eo=$k[--$j];if(_Eo!=1||$g($_.msg,$_.i)>127){$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws()}$_.mode=$_.codeone_a;break}if($_.i+1<$_.msglen){if(!$has($_.xvals,$g($_.msg,$_.i+1))){break}if($_.i+2<$_.msglen){if(!$has($_.xvals,$g($_.msg,$_.i+2))){break}}}}else{$_.lookup();var _F8=$k[--$j];if(_F8!=$_.mode){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws();$_.mode=$_.codeone_a;break}}if($_.msglen-$_.i<=3){$k[$j++]=$_.j+~~($_.p/3)*2;$_.getnumremcws();var _FH=$k[--$j];$_.remcws=_FH;$k[$j++]=Infinity;var _FM=$G($_.msg,$_.i,$_.msglen-$_.i);for(var _FN=0,_FO=_FM.length;_FN<_FO;_FN++){var _FP=$g(_FM,_FN);var _FT=$has($g($_.encvals,$_.mode),_FP);$k[$j++]=_FP;if(_FT){var _FX=$k[--$j];$q($g($g($_.encvals,$_.mode),_FX))}else{$k[$j-1]=-1;$k[$j++]=-1;$k[$j++]=-1;$k[$j++]=-1}}$_.remvals=$a();if($_.remcws==2&&$_.remvals.length==3){$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));$q($_.remvals);var _Ff=$a();$k[$j++]=_Ff;$_.CTXvalstocws();$_.addtocws();$_.mode=$_.codeone_a;$_.i=$_.msglen;break}if($_.remcws==2&&$_.remvals.length==2&&$_.mode!=$_.codeone_x){$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));$q($_.remvals);$q($g($g($_.encvals,$_.mode),$_.codeone_sft1));var _Fs=$a();$k[$j++]=_Fs;$_.CTXvalstocws();$_.addtocws();$_.mode=$_.codeone_a;$_.i=$_.msglen;break}if($_.remcws==2&&$_.remvals.length==1){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws();$k[$j++]=$g($_.avals,$g($_.msg,$_.i));$_.addtocws();$_.mode=$_.codeone_a;$_.i=$_.msglen;break}if($_.remcws==1&&$_.remvals.length==1){$k[$j++]=$G($_.ctxvals,0,$_.p);$_.CTXvalstocws();$_.addtocws();$k[$j++]=$g($_.avals,$g($_.msg,$_.i));$_.addtocws();$_.mode=$_.codeone_a;$_.i=$_.msglen;break}}}var _GN=$g($g($_.encvals,$_.mode),$g($_.msg,$_.i));$k[$j++]=_GN;if(_GN.length+$_.p>$_.ctxvals.length){$k[$j-1]="bwipp.codeoneTooLong#31568";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}var _GQ=$k[--$j];$P($_.ctxvals,$_.p,_GQ);$_.p=_GQ.length+$_.p;$_.i=$_.i+1}if($_.mode!=$_.codeone_a){for(;;){if($_.p%3==0){break}$_.i=$_.i-1;$_.p=$_.p-$g($g($_.encvals,$_.mode),$g($_.msg,$_.i)).length}$k[$j++]=Infinity;$q($G($_.ctxvals,0,$_.p));var _Gj=$a();$k[$j++]=_Gj;$_.CTXvalstocws();$_.addtocws();$k[$j++]=$a([$_.codeone_unlcw]);$_.addtocws();$_.mode=$_.codeone_a;if($_.i!=$_.msglen){if($g($_.numD,$_.i)>=2){var _Gq=$s(2);$p(_Gq,0,$g($_.msg,$_.i));$p(_Gq,1,$g($_.msg,$_.i+1));$k[$j++]=$g($_.avals,_Gq);$_.addtocws();$_.i=$_.i+2}else{$k[$j++]=$g($_.avals,$g($_.msg,$_.i));$_.addtocws();$_.i=$_.i+1}}}};$_.encD=function(){for(;;){if($g($_.numD,$_.i)<3){$_.Drem=(8-$_.Dbitslen%8)%8;$k[$j++]=$_.j+~~($_.Dbitslen/8);$_.getnumremcws();var _HC=$k[--$j];$_.remcws=_HC;$k[$j++]=$_.j+~~($_.Dbitslen/8)-1;$_.getnumremcws();var _HF=$k[--$j];if((_HF-1==0&&$_.Drem==0||$_.remcws==1&&$_.Drem!=0)&&$_.i==$_.msglen){if($_.Drem==4||$_.Drem==6){$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=4;$_.appendbits()}if($_.Drem==2||$_.Drem==6){$k[$j++]=0;$k[$j++]=1;$k[$j++]=2;$_.appendbits()}break}if(($_.i==$_.msglen-1&&$g($_.numD,$_.i)==1||$_.i==$_.msglen-2&&$g($_.numD,$_.i)==2)&&$_.remcws==1&&$_.Drem==0){break}if(!($_.i==$_.msglen-1&&$g($_.numD,$_.i)==1&&$_.remcws==1&&($_.Drem==4||$_.Drem==6))){$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=6;$_.appendbits();$_.Drem=(8-$_.Dbitslen%8)%8}if($_.Drem==4||$_.Drem==6){if($g($_.numD,$_.i)>=1){$k[$j++]=$g($_.msg,$_.i)-48+1;$k[$j++]=4;$_.tobin();var _Hs=$k[--$j];$q(_Hs);$k[$j++]=4;$_.appendbits();$_.i=$_.i+1}else{$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=4;$_.appendbits()}$_.Drem=$_.Drem-4}if($_.Drem==2){$k[$j++]=0;$k[$j++]=1;$k[$j++]=2;$_.appendbits();$_.Drem=0}break}var _Hy=$G($_.msg,$_.i,3);var _I3=0;for(var _Hz=0,_I0=_Hy.length;_Hz<_I0;_Hz++){_I3=$f(_I3+($g(_Hy,_Hz)-48))*10}$k[$j++]=~~(_I3/10)+1;$k[$j++]=10;$_.tobin();var _I4=$k[--$j];$q(_I4);$k[$j++]=10;$_.appendbits();$_.i=$_.i+3}$_.Dbits=$G($_.Dbitsbuf,0,$_.Dbitslen);$k[$j++]=Infinity;if($_.Dbitslen>0){for(var _IC=0,_IB=$_.Dbitslen-1;_IC<=_IB;_IC+=8){var _IE=$G($_.Dbits,_IC,8);var _IJ=0;for(var _IF=0,_IG=_IE.length;_IF<_IG;_IF++){_IJ=$f(_IJ+$g(_IE,_IF))*2}$k[$j++]=~~(_IJ/2)}}$as($a($m()));var _IM=$k[--$j];$k[$j-1]=_IM;$_.addtocws();$_.mode=$_.codeone_a};$_.encB=function(){$_.p=0;$_.bvals=$a(1480);for(;;){if($_.i==$_.msglen){break}if($g($_.msg,$_.i)<0){break}$_.lookup();var _IV=$k[--$j];if(_IV!=$_.mode){break}if($_.p>=$_.bvals.length){$k[$j++]="bwipp.codeoneTooLong#31690";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}$p($_.bvals,$_.p,$g($_.msg,$_.i));$_.p=$_.p+1;$_.i=$_.i+1}$k[$j++]=$_.j+$_.p;$_.getnumremcws();var _Ih=$k[--$j];$_.remcws=_Ih-1;$k[$j++]=Infinity;if($_.remcws==0&&$_.i==$_.msglen){$k[$j++]=0}else{if($_.p<250){$k[$j++]=$_.p}else{$k[$j++]=~~($_.p/250)+249;$k[$j++]=$_.p%250}}$q($G($_.bvals,0,$_.p));$_.bvals=$a();$k[$j++]=$_.bvals;$_.addtocws();$_.mode=$_.codeone_a};$_.cws=$a(1480);$_.mode=$_.codeone_a;$_.i=0;$_.j=0;for(;;){if($_.i>=$_.msglen){break}var _Iy=$_[$g($_.codeone_encfuncs,$_.mode)];if(_Iy()===true){break}}$_.cws=$G($_.cws,0,$_.j)}var _J2=$_.metrics;for(var _J3=0,_J4=_J2.length;_J3<_J4;_J3++){$_.m=$g(_J2,_J3);$_.vers=$g($_.m,0);$_.rows=$g($_.m,1);$_.cols=$g($_.m,2);$_.dcol=$g($_.m,3);$_.dcws=$g($_.m,4);$_.rscw=$g($_.m,5);$_.rsbl=$g($_.m,6);$_.riso=$g($_.m,7);$_.risi=$g($_.m,8);$_.risl=$g($_.m,9);$_.dcpb=~~($_.dcws/$_.rsbl);$_.ecpb=~~($_.rscw/$_.rsbl);$_.okay=true;if($ne($_.version,"unset")&&$ne($_.version,$_.vers)){$_.okay=false}if($_.cws.length>$_.dcws){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.codeoneNoValidSymbol#31747";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}if(!$_.stype){$k[$j++]=Infinity;$q($_.cws);for(var _Jf=0,_Jg=$f($_.dcws-$_.cws.length);_Jf<_Jg;_Jf++){$k[$j++]=129}$_.cws=$a()}else{$k[$j++]=Infinity;for(var _Jj=0,_Jk=$f($_.dcws-$_.cws.length);_Jj<_Jk;_Jj++){$k[$j++]=0}$q($_.cws);$_.cws=$a()}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#31761";$k[$j++]=$_.cws;bwipp_raiseerror()}var _Jq=$_.stype?5:8;$_.rsbt=_Jq;$k[$j++]="coeffs";$k[$j++]=$_.ecpb;$k[$j++]=$_.rsbt;$_.codeone_coeffscachefetch();var _Jt=$k[--$j];var _Ju=$k[--$j];$_[_Ju]=_Jt;$_.ecbs=$a($_.rsbl);for(var _Jz=0,_Jy=$_.rsbl-1;_Jz<=_Jy;_Jz+=1){$_.blk=_Jz;$_.lfsr=$a($_.ecpb);var _K4=$_.dcpb;var _K5=$_.coeffs;var _K6=$_.ecpb;var _K7=$_.lfsr;$k[$j++]=$_.ecbs;$k[$j++]=$_.blk;$k[$j++]=function(){var _K9=$k[--$j];$k[$j++]=$g($_.cws,$f(_K9*$_.rsbl+$_.blk))};$k[$j++]=_K4;$k[$j++]=_K5;$k[$j++]=_K6;$k[$j++]=_K7;$k[$j++]=$_.codeone_rsprod;bwipp_rsecbinary();var _KE=$k[--$j];var _KF=$k[--$j];$p(_KF,_KE,$_.lfsr)}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#31789";$k[$j++]=Infinity;var _KI=$_.ecbs;for(var _KJ=0,_KK=_KI.length;_KJ<_KK;_KJ++){$q($g(_KI,_KJ))}var _KM=$a();$k[$j++]=_KM;bwipp_raiseerror()}var _KP=$a($f($_.cws.length+$_.rscw));$P(_KP,0,$_.cws);$_.cws=_KP;for(var _KT=0,_KS=$_.rscw-1;_KT<=_KS;_KT+=1){$_.i=_KT;$p($_.cws,$f($_.dcws+$_.i),$g($g($_.ecbs,$_.i%$_.rsbl),~~($_.i/$_.rsbl)))}var _Kh=$_.stype?5:8;$_.mmat=$a($f($_.dcws+$_.rscw)*_Kh);$_.r=0;$_.c=0;var _Kk=$_.stype?2:1;for(var _Kn=0,_Ko=_Kk,_Km=$_.cws.length-1;_Ko<0?_Kn>=_Km:_Kn<=_Km;_Kn+=_Ko){$_.i=_Kn;if(!$_.stype){var _Kr=$Z($s(8),"00000000");var _Kw=$R($s(8),$g($_.cws,$_.i),2);$P(_Kr,8-_Kw.length,_Kw);$k[$j++]=Infinity;$F(_Kr,function(){$k[$j-1]-=48});var _L0=$a();$_.top=$G(_L0,0,4);$_.bot=$G(_L0,4,4)}else{var _L4=$Z($s(5),"00000");var _L9=$R($s(5),$g($_.cws,$_.i),2);$P(_L4,5-_L9.length,_L9);$k[$j++]=Infinity;$F(_L4,function(){$k[$j-1]-=48});var _LD=$a();$_.c1=_LD;var _LF=$Z($s(5),"00000");var _LK=$R($s(5),$g($_.cws,$_.i+1),2);$P(_LF,5-_LK.length,_LK);$k[$j++]=Infinity;$F(_LF,function(){$k[$j-1]-=48});var _LO=$a();$_.c2=_LO;$k[$j++]=Infinity;$q($G($_.c1,0,3));$q($G($_.c2,0,2));$_.top=$a();$k[$j++]=Infinity;$q($G($_.c1,3,2));$q($G($_.c2,2,3));$_.bot=$a()}$P($_.mmat,$f($_.r*$_.dcol+$_.c),$_.top);$P($_.mmat,$f(($_.r+1)*$_.dcol+$_.c),$_.bot);$_.c=$_.c+$_.top.length;if($_.c==$_.dcol){$_.c=0;$_.r=$_.r+2}}$_.mmv=function(){var _Ln=$k[--$j];var _Lo=$k[--$j];$k[$j++]=$f(_Lo+_Ln*$_.cols)};$k[$j++]=Infinity;for(var _Lr=0,_Ls=$_.rows*$_.cols;_Lr<_Ls;_Lr++){$k[$j++]=-1}$_.pixs=$a();$_.artifact=$a([function(){for(var _Lu=0,_Lv=$_.cols;_Lu<_Lv;_Lu++){$k[$j++]=0}},function(){for(var _Lx=0,_Ly=$_.cols;_Lx<_Ly;_Lx++){$k[$j++]=1}},function(){$k[$j++]=0;for(var _M0=0,_M1=$_.cols-2;_M0<_M1;_M0++){$k[$j++]=1}$k[$j++]=0},function(){$k[$j++]=0;$k[$j++]=1;for(var _M3=0,_M4=$_.cols-4;_M3<_M4;_M3++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0},function(){for(var _M6=0,_M7=~~(($_.cols-1)/2);_M6<_M7;_M6++){$k[$j++]=-1}$k[$j++]=1;for(var _M9=0,_MA=~~(($_.cols-1)/2);_M9<_MA;_M9++){$k[$j++]=-1}},function(){for(var _MC=0,_MD=~~(($_.cols-1)/2);_MC<_MD;_MC++){$k[$j++]=-1}$k[$j++]=0;for(var _MF=0,_MG=~~(($_.cols-1)/2);_MF<_MG;_MF++){$k[$j++]=-1}},function(){$k[$j++]=1;for(var _MI=0,_MJ=$_.cols-2;_MI<_MJ;_MI++){$k[$j++]=0}$k[$j++]=1},function(){$k[$j++]=1;$k[$j++]=0;for(var _ML=0,_MM=$_.cols-4;_ML<_MM;_ML++){$k[$j++]=1}$k[$j++]=0;$k[$j++]=1}]);$_.cpat=$g($_.codeone_cpatmap,$G($_.vers,0,1));$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=~~($f($_.rows-$_.cpat.length)/2);$_.mmv();$k[$j++]=Infinity;$F($_.cpat,function(){var _MV=$k[--$j];if($g($_.artifact,_MV-49)()===true){return true}});var _MX=$a();var _MY=$k[--$j];var _MZ=$k[--$j];$P(_MZ,_MY,_MX);for(var _Mc=0,_Mb=$_.risl-1;_Mc<=_Mb;_Mc+=1){$_.i=_Mc;for(var _Mh=$_.riso,_Mi=$_.risi,_Mg=$_.cols-1;_Mi<0?_Mh>=_Mg:_Mh<=_Mg;_Mh+=_Mi){$_.j=_Mh;var _Mk=$_.i%12==0?1:0;var _Ml=$a([1,_Mk]);$k[$j++]=_Ml;$k[$j++]=_Ml;$k[$j++]=$_.pixs;$k[$j++]=$_.j;$k[$j++]=$_.i;$_.mmv();var _Mp=$k[$j-3];var _Mq=$k[$j-1];var _Mr=$k[$j-2];$j-=3;$P(_Mr,_Mq,_Mp);if($_.i!=$_.risl-1){$k[$j++]=$_.pixs;$k[$j++]=$f($_.cols-$_.j)-2;$k[$j++]=$f($_.rows-$_.i)-1;$_.mmv();var _Mz=$k[$j-3];var _N0=$k[$j-1];var _N1=$k[$j-2];$j-=3;$P(_N1,_N0,_Mz)}else{$j--}}}$F($g($_.codeone_blackdotmap,$_.vers),function(){var _N5=$k[--$j];$k[$j++]=$_.pixs;$q(_N5);$_.mmv();var _N6=$k[--$j];var _N7=$k[--$j];$p(_N7,_N6,1)});$_.j=0;for(var _NA=0,_N9=$_.pixs.length-1;_NA<=_N9;_NA+=1){$_.i=_NA;if($g($_.pixs,$_.i)==-1){$p($_.pixs,$_.i,$g($_.mmat,$_.j));$_.j=$_.j+1}}$k[$j++]=Infinity;$k[$j++]="ren";$k[$j++]="renmatrix";$k[$j++]="pixs";$k[$j++]=$_.pixs;$k[$j++]="pixx";$k[$j++]=$_.cols;$k[$j++]="pixy";$k[$j++]=$_.rows;$k[$j++]="height";$k[$j++]=$_.rows*$_.modunit/72;$k[$j++]="width";$k[$j++]=$_.cols*$_.modunit/72;if($_.stype||$_.ttype){$k[$j++]="borderleft";$k[$j++]=1;$k[$j++]="borderright";$k[$j++]=1;$k[$j++]="bordertop";$k[$j++]=0;$k[$j++]="borderbottom";$k[$j++]=1}$k[$j++]="opt";$k[$j++]=$_.options;var _NU=$d();$k[$j++]=_NU;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hanxin(){if(!bwipp_hanxin.globals){var $__=$_;$_=bwipp_hanxin.globals={};$_.hanxin_noblk=$a([0,-1,-1]);$_.hanxin_metrics=$a([$a(["1",23,-1,0,205,$a([1,21,4]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,17,8]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,13,12]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,9,16]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["2",25,-1,0,301,$a([1,31,6]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,25,12]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,19,18]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,15,22]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["3",27,-1,0,405,$a([1,42,8]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,34,16]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,26,24]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,20,30]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["4",29,14,1,439,$a([1,46,8]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,38,16]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,30,24]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,22,32]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["5",31,16,1,555,$a([1,57,12]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,49,20]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,37,32]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,14,20]),$a([1,13,22]),$_.hanxin_noblk]),$a(["6",33,16,1,675,$a([1,70,14]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,58,26]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,24,20]),$a([1,22,18]),$_.hanxin_noblk,$a([1,16,24]),$a([1,18,26]),$_.hanxin_noblk]),$a(["7",35,17,1,805,$a([1,84,16]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,70,30]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,26,22]),$a([1,28,24]),$_.hanxin_noblk,$a([2,14,20]),$a([1,12,20]),$_.hanxin_noblk]),$a(["8",37,18,1,943,$a([1,99,18]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,40,18]),$a([1,41,18]),$_.hanxin_noblk,$a([1,31,26]),$a([1,32,28]),$_.hanxin_noblk,$a([2,16,24]),$a([1,15,22]),$_.hanxin_noblk]),$a(["9",39,19,1,1089,$a([1,114,22]),$_.hanxin_noblk,$_.hanxin_noblk,$a([2,48,20]),$_.hanxin_noblk,$_.hanxin_noblk,$a([2,24,20]),$a([1,26,22]),$_.hanxin_noblk,$a([2,18,28]),$a([1,18,26]),$_.hanxin_noblk]),$a(["10",41,20,1,1243,$a([1,131,24]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,52,22]),$a([1,57,24]),$_.hanxin_noblk,$a([2,27,24]),$a([1,29,24]),$_.hanxin_noblk,$a([2,21,32]),$a([1,19,30]),$_.hanxin_noblk]),$a(["11",43,14,2,1289,$a([1,135,26]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,56,24]),$a([1,57,24]),$_.hanxin_noblk,$a([2,28,24]),$a([1,31,26]),$_.hanxin_noblk,$a([2,22,32]),$a([1,21,32]),$_.hanxin_noblk]),$a(["12",45,15,2,1455,$a([1,153,28]),$_.hanxin_noblk,$_.hanxin_noblk,$a([1,62,26]),$a([1,65,28]),$_.hanxin_noblk,$a([2,32,28]),$a([1,33,28]),$_.hanxin_noblk,$a([3,17,26]),$a([1,22,30]),$_.hanxin_noblk]),$a(["13",47,16,2,1629,$a([1,86,16]),$a([1,85,16]),$_.hanxin_noblk,$a([1,71,30]),$a([1,72,30]),$_.hanxin_noblk,$a([2,37,32]),$a([1,35,30]),$_.hanxin_noblk,$a([3,20,30]),$a([1,21,32]),$_.hanxin_noblk]),$a(["14",49,16,2,1805,$a([1,94,18]),$a([1,95,18]),$_.hanxin_noblk,$a([2,51,22]),$a([1,55,24]),$_.hanxin_noblk,$a([3,30,26]),$a([1,31,26]),$_.hanxin_noblk,$a([4,18,28]),$a([1,17,24]),$_.hanxin_noblk]),$a(["15",51,17,2,1995,$a([1,104,20]),$a([1,105,20]),$_.hanxin_noblk,$a([2,57,24]),$a([1,61,26]),$_.hanxin_noblk,$a([3,33,28]),$a([1,36,30]),$_.hanxin_noblk,$a([4,20,30]),$a([1,19,30]),$_.hanxin_noblk]),$a(["16",53,17,2,2187,$a([1,115,22]),$a([1,114,22]),$_.hanxin_noblk,$a([2,65,28]),$a([1,61,26]),$_.hanxin_noblk,$a([3,38,32]),$a([1,33,30]),$_.hanxin_noblk,$a([5,19,28]),$a([1,14,24]),$_.hanxin_noblk]),$a(["17",55,18,2,2393,$a([1,126,24]),$a([1,125,24]),$_.hanxin_noblk,$a([2,70,30]),$a([1,69,30]),$_.hanxin_noblk,$a([4,33,28]),$a([1,29,26]),$_.hanxin_noblk,$a([5,20,30]),$a([1,19,30]),$_.hanxin_noblk]),$a(["18",57,19,2,2607,$a([1,136,26]),$a([1,137,26]),$_.hanxin_noblk,$a([3,56,24]),$a([1,59,26]),$_.hanxin_noblk,$a([5,35,30]),$_.hanxin_noblk,$_.hanxin_noblk,$a([6,18,28]),$a([1,21,28]),$_.hanxin_noblk]),$a(["19",59,20,2,2829,$a([1,148,28]),$a([1,149,28]),$_.hanxin_noblk,$a([3,61,26]),$a([1,64,28]),$_.hanxin_noblk,$a([7,24,20]),$a([1,23,22]),$_.hanxin_noblk,$a([6,20,30]),$a([1,21,32]),$_.hanxin_noblk]),$a(["20",61,20,2,3053,$a([3,107,20]),$_.hanxin_noblk,$_.hanxin_noblk,$a([3,65,28]),$a([1,72,30]),$_.hanxin_noblk,$a([7,26,22]),$a([1,23,22]),$_.hanxin_noblk,$a([7,19,28]),$a([1,20,32]),$_.hanxin_noblk]),$a(["21",63,21,2,3291,$a([3,115,22]),$_.hanxin_noblk,$_.hanxin_noblk,$a([4,56,24]),$a([1,63,28]),$_.hanxin_noblk,$a([7,28,24]),$a([1,25,22]),$_.hanxin_noblk,$a([8,18,28]),$a([1,21,22]),$_.hanxin_noblk]),$a(["22",65,16,3,3383,$a([2,116,22]),$a([1,122,24]),$_.hanxin_noblk,$a([4,56,24]),$a([1,72,30]),$_.hanxin_noblk,$a([7,28,24]),$a([1,32,26]),$_.hanxin_noblk,$a([8,18,28]),$a([1,24,30]),$_.hanxin_noblk]),$a(["23",67,17,3,3631,$a([3,127,24]),$_.hanxin_noblk,$_.hanxin_noblk,$a([5,51,22]),$a([1,62,26]),$_.hanxin_noblk,$a([7,30,26]),$a([1,35,26]),$_.hanxin_noblk,$a([8,20,30]),$a([1,21,32]),$_.hanxin_noblk]),$a(["24",69,17,3,3887,$a([2,135,26]),$a([1,137,26]),$_.hanxin_noblk,$a([5,56,24]),$a([1,59,26]),$_.hanxin_noblk,$a([7,33,28]),$a([1,30,28]),$_.hanxin_noblk,$a([11,16,24]),$a([1,19,26]),$_.hanxin_noblk]),$a(["25",71,18,3,4151,$a([3,105,20]),$a([1,121,22]),$_.hanxin_noblk,$a([5,61,26]),$a([1,57,26]),$_.hanxin_noblk,$a([9,28,24]),$a([1,28,22]),$_.hanxin_noblk,$a([10,19,28]),$a([1,18,30]),$_.hanxin_noblk]),$a(["26",73,18,3,4423,$a([2,157,30]),$a([1,150,28]),$_.hanxin_noblk,$a([5,65,28]),$a([1,61,26]),$_.hanxin_noblk,$a([8,33,28]),$a([1,34,30]),$_.hanxin_noblk,$a([10,19,28]),$a([2,15,26]),$_.hanxin_noblk]),$a(["27",75,19,3,4703,$a([3,126,24]),$a([1,115,22]),$_.hanxin_noblk,$a([7,51,22]),$a([1,54,22]),$_.hanxin_noblk,$a([8,35,30]),$a([1,37,30]),$_.hanxin_noblk,$a([15,15,22]),$a([1,10,22]),$_.hanxin_noblk]),$a(["28",77,19,3,4991,$a([4,105,20]),$a([1,103,20]),$_.hanxin_noblk,$a([7,56,24]),$a([1,45,18]),$_.hanxin_noblk,$a([10,31,26]),$a([1,27,26]),$_.hanxin_noblk,$a([10,17,26]),$a([3,20,28]),$a([1,21,28])]),$a(["29",79,20,3,5287,$a([3,139,26]),$a([1,137,28]),$_.hanxin_noblk,$a([6,66,28]),$a([1,66,30]),$_.hanxin_noblk,$a([9,36,30]),$a([1,34,32]),$_.hanxin_noblk,$a([13,19,28]),$a([1,17,32]),$_.hanxin_noblk]),$a(["30",81,20,3,5591,$a([6,84,16]),$a([1,82,16]),$_.hanxin_noblk,$a([6,70,30]),$a([1,68,30]),$_.hanxin_noblk,$a([7,35,30]),$a([3,33,28]),$a([1,32,28]),$a([13,20,30]),$a([1,20,28]),$_.hanxin_noblk]),$a(["31",83,21,3,5903,$a([5,105,20]),$a([1,94,18]),$_.hanxin_noblk,$a([6,74,32]),$a([1,71,30]),$_.hanxin_noblk,$a([11,33,28]),$a([1,34,32]),$_.hanxin_noblk,$a([13,19,28]),$a([3,16,26]),$_.hanxin_noblk]),$a(["32",85,17,4,6033,$a([4,127,24]),$a([1,126,24]),$_.hanxin_noblk,$a([7,66,28]),$a([1,66,30]),$_.hanxin_noblk,$a([12,30,24]),$a([1,24,28]),$a([1,24,30]),$a([15,19,28]),$a([1,17,32]),$_.hanxin_noblk]),$a(["33",87,17,4,6353,$a([7,84,16]),$a([1,78,16]),$_.hanxin_noblk,$a([7,70,30]),$a([1,66,28]),$_.hanxin_noblk,$a([12,33,28]),$a([1,32,30]),$_.hanxin_noblk,$a([14,21,32]),$a([1,24,28]),$_.hanxin_noblk]),$a(["34",89,18,4,6689,$a([5,117,22]),$a([1,117,24]),$_.hanxin_noblk,$a([8,66,28]),$a([1,58,26]),$_.hanxin_noblk,$a([11,38,32]),$a([1,34,32]),$_.hanxin_noblk,$a([15,20,30]),$a([2,17,26]),$_.hanxin_noblk]),$a(["35",91,18,4,7025,$a([4,148,28]),$a([1,146,28]),$_.hanxin_noblk,$a([8,68,30]),$a([1,70,24]),$_.hanxin_noblk,$a([10,36,32]),$a([3,38,28]),$_.hanxin_noblk,$a([16,19,28]),$a([3,16,26]),$_.hanxin_noblk]),$a(["36",93,19,4,7377,$a([4,126,24]),$a([2,135,26]),$_.hanxin_noblk,$a([8,70,28]),$a([2,43,26]),$_.hanxin_noblk,$a([13,32,28]),$a([2,41,30]),$_.hanxin_noblk,$a([17,19,28]),$a([3,15,26]),$_.hanxin_noblk]),$a(["37",95,19,4,7729,$a([5,136,26]),$a([1,132,24]),$_.hanxin_noblk,$a([5,67,30]),$a([4,68,28]),$a([1,69,28]),$a([14,35,30]),$a([1,32,24]),$_.hanxin_noblk,$a([18,18,26]),$a([3,16,28]),$a([1,14,28])]),$a(["38",97,19,4,8089,$a([3,142,26]),$a([3,141,28]),$_.hanxin_noblk,$a([8,70,30]),$a([1,73,32]),$a([1,74,32]),$a([12,34,30]),$a([3,34,26]),$a([1,35,28]),$a([18,21,32]),$a([1,27,30]),$_.hanxin_noblk]),$a(["39",99,20,4,8465,$a([5,116,22]),$a([2,103,20]),$a([1,102,20]),$a([9,74,32]),$a([1,74,30]),$_.hanxin_noblk,$a([14,34,28]),$a([2,32,32]),$a([1,32,30]),$a([19,21,32]),$a([1,25,26]),$_.hanxin_noblk]),$a(["40",101,20,4,8841,$a([7,116,22]),$a([1,117,22]),$_.hanxin_noblk,$a([11,65,28]),$a([1,58,24]),$_.hanxin_noblk,$a([15,38,32]),$a([1,27,28]),$_.hanxin_noblk,$a([20,20,30]),$a([1,20,32]),$a([1,21,32])]),$a(["41",103,17,5,9009,$a([6,136,26]),$a([1,130,24]),$_.hanxin_noblk,$a([11,66,28]),$a([1,62,30]),$_.hanxin_noblk,$a([14,34,28]),$a([3,34,32]),$a([1,30,30]),$a([18,20,30]),$a([3,20,28]),$a([2,15,26])]),$a(["42",105,17,5,9401,$a([5,105,20]),$a([2,115,22]),$a([2,116,22]),$a([10,75,32]),$a([1,73,32]),$_.hanxin_noblk,$a([16,38,32]),$a([1,27,28]),$_.hanxin_noblk,$a([22,19,28]),$a([2,16,30]),$a([1,19,30])]),$a(["43",107,18,5,9799,$a([6,147,28]),$a([1,146,28]),$_.hanxin_noblk,$a([11,66,28]),$a([2,65,30]),$_.hanxin_noblk,$a([18,33,28]),$a([2,33,30]),$_.hanxin_noblk,$a([22,21,32]),$a([1,28,30]),$_.hanxin_noblk]),$a(["44",109,18,5,10207,$a([6,116,22]),$a([3,125,24]),$_.hanxin_noblk,$a([11,75,32]),$a([1,68,30]),$_.hanxin_noblk,$a([13,35,28]),$a([6,34,32]),$a([1,30,30]),$a([23,21,32]),$a([1,26,30]),$_.hanxin_noblk]),$a(["45",111,18,5,10623,$a([7,105,20]),$a([4,95,18]),$_.hanxin_noblk,$a([12,67,28]),$a([1,63,30]),$a([1,62,32]),$a([21,31,26]),$a([2,33,32]),$_.hanxin_noblk,$a([23,21,32]),$a([2,24,30]),$_.hanxin_noblk]),$a(["46",113,19,5,11045,$a([10,116,22]),$_.hanxin_noblk,$_.hanxin_noblk,$a([12,74,32]),$a([1,78,30]),$_.hanxin_noblk,$a([18,37,32]),$a([1,39,30]),$a([1,41,28]),$a([25,21,32]),$a([1,27,28]),$_.hanxin_noblk]),$a(["47",115,19,5,11477,$a([5,126,24]),$a([4,115,22]),$a([1,114,22]),$a([12,67,28]),$a([2,66,32]),$a([1,68,30]),$a([21,35,30]),$a([1,39,30]),$_.hanxin_noblk,$a([26,21,32]),$a([1,28,28]),$_.hanxin_noblk]),$a(["48",117,19,5,11917,$a([9,126,24]),$a([1,117,22]),$_.hanxin_noblk,$a([13,75,32]),$a([1,68,30]),$_.hanxin_noblk,$a([20,35,30]),$a([3,35,28]),$_.hanxin_noblk,$a([27,21,32]),$a([1,28,30]),$_.hanxin_noblk]),$a(["49",119,17,6,12111,$a([9,126,24]),$a([1,137,26]),$_.hanxin_noblk,$a([13,71,30]),$a([2,68,32]),$_.hanxin_noblk,$a([20,37,32]),$a([1,39,28]),$a([1,38,28]),$a([24,20,32]),$a([5,25,28]),$_.hanxin_noblk]),$a(["50",121,17,6,12559,$a([8,147,28]),$a([1,141,28]),$_.hanxin_noblk,$a([10,73,32]),$a([4,74,30]),$a([1,73,30]),$a([16,36,32]),$a([6,39,30]),$a([1,37,30]),$a([27,21,32]),$a([3,20,26]),$_.hanxin_noblk]),$a(["51",123,18,6,13025,$a([9,137,26]),$a([1,135,26]),$_.hanxin_noblk,$a([12,70,30]),$a([4,75,32]),$_.hanxin_noblk,$a([24,35,30]),$a([1,40,28]),$_.hanxin_noblk,$a([23,20,32]),$a([8,24,30]),$_.hanxin_noblk]),$a(["52",125,18,6,13489,$a([14,95,18]),$a([1,86,18]),$_.hanxin_noblk,$a([13,73,32]),$a([3,77,30]),$_.hanxin_noblk,$a([24,35,30]),$a([2,35,28]),$_.hanxin_noblk,$a([26,21,32]),$a([5,21,30]),$a([1,23,30])]),$a(["53",127,18,6,13961,$a([9,147,28]),$a([1,142,28]),$_.hanxin_noblk,$a([10,73,30]),$a([6,70,32]),$a([1,71,32]),$a([25,35,30]),$a([2,34,26]),$_.hanxin_noblk,$a([29,21,32]),$a([4,22,30]),$_.hanxin_noblk]),$a(["54",129,18,6,14441,$a([11,126,24]),$a([1,131,24]),$_.hanxin_noblk,$a([16,74,32]),$a([1,79,30]),$_.hanxin_noblk,$a([25,38,32]),$a([1,25,30]),$_.hanxin_noblk,$a([33,21,32]),$a([1,28,28]),$_.hanxin_noblk]),$a(["55",131,19,6,14939,$a([14,105,20]),$a([1,99,18]),$_.hanxin_noblk,$a([19,65,28]),$a([1,72,28]),$_.hanxin_noblk,$a([24,37,32]),$a([2,40,30]),$a([1,41,30]),$a([31,21,32]),$a([4,24,32]),$_.hanxin_noblk]),$a(["56",133,19,6,15435,$a([10,147,28]),$a([1,151,28]),$_.hanxin_noblk,$a([15,71,30]),$a([3,71,32]),$a([1,73,32]),$a([24,37,32]),$a([3,38,30]),$a([1,39,30]),$a([36,19,30]),$a([3,29,26]),$_.hanxin_noblk]),$a(["57",135,19,6,15939,$a([15,105,20]),$a([1,99,18]),$_.hanxin_noblk,$a([19,70,30]),$a([1,64,28]),$_.hanxin_noblk,$a([27,38,32]),$a([2,25,26]),$_.hanxin_noblk,$a([38,20,30]),$a([2,18,28]),$_.hanxin_noblk]),$a(["58",137,17,7,16171,$a([14,105,20]),$a([1,113,22]),$a([1,114,22]),$a([17,67,30]),$a([3,92,32]),$_.hanxin_noblk,$a([30,35,30]),$a([1,41,30]),$_.hanxin_noblk,$a([36,21,32]),$a([1,26,30]),$a([1,27,30])]),$a(["59",139,17,7,16691,$a([11,146,28]),$a([1,146,26]),$_.hanxin_noblk,$a([20,70,30]),$a([1,60,26]),$_.hanxin_noblk,$a([29,38,32]),$a([1,24,32]),$_.hanxin_noblk,$a([40,20,30]),$a([2,17,26]),$_.hanxin_noblk]),$a(["60",141,18,7,17215,$a([3,137,26]),$a([1,136,26]),$a([10,126,24]),$a([22,65,28]),$a([1,75,30]),$_.hanxin_noblk,$a([30,37,32]),$a([1,51,30]),$_.hanxin_noblk,$a([42,20,30]),$a([1,21,30]),$_.hanxin_noblk]),$a(["61",143,18,7,17751,$a([12,126,24]),$a([2,118,22]),$a([1,116,22]),$a([19,74,32]),$a([1,74,30]),$a([1,72,28]),$a([30,38,32]),$a([2,29,30]),$_.hanxin_noblk,$a([39,20,32]),$a([2,37,26]),$a([1,38,26])]),$a(["62",145,18,7,18295,$a([12,126,24]),$a([3,136,26]),$_.hanxin_noblk,$a([21,70,30]),$a([2,65,28]),$_.hanxin_noblk,$a([34,35,30]),$a([1,44,32]),$_.hanxin_noblk,$a([42,20,30]),$a([2,19,28]),$a([2,18,28])]),$a(["63",147,18,7,18847,$a([12,126,24]),$a([3,117,22]),$a([1,116,22]),$a([25,61,26]),$a([2,62,28]),$_.hanxin_noblk,$a([34,35,30]),$a([1,40,32]),$a([1,41,32]),$a([45,20,30]),$a([1,20,32]),$a([1,21,32])]),$a(["64",149,19,7,19403,$a([15,105,20]),$a([2,115,22]),$a([2,116,22]),$a([25,65,28]),$a([1,72,28]),$_.hanxin_noblk,$a([18,35,30]),$a([17,37,32]),$a([1,50,32]),$a([42,20,30]),$a([6,19,28]),$a([1,15,28])]),$a(["65",151,19,7,19971,$a([19,105,20]),$a([1,101,20]),$_.hanxin_noblk,$a([33,51,22]),$a([1,65,22]),$_.hanxin_noblk,$a([40,33,28]),$a([1,28,28]),$_.hanxin_noblk,$a([49,20,30]),$a([1,18,28]),$_.hanxin_noblk]),$a(["66",153,17,8,20229,$a([18,105,20]),$a([2,117,22]),$_.hanxin_noblk,$a([26,65,28]),$a([1,80,30]),$_.hanxin_noblk,$a([35,35,30]),$a([3,35,28]),$a([1,36,28]),$a([52,18,28]),$a([2,38,30]),$_.hanxin_noblk]),$a(["67",155,17,8,20805,$a([26,84,16]),$_.hanxin_noblk,$_.hanxin_noblk,$a([26,70,30]),$_.hanxin_noblk,$_.hanxin_noblk,$a([45,31,26]),$a([1,9,26]),$_.hanxin_noblk,$a([52,20,30]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["68",157,17,8,21389,$a([16,126,24]),$a([1,114,22]),$a([1,115,22]),$a([23,70,30]),$a([3,65,28]),$a([1,66,28]),$a([40,35,30]),$a([1,43,30]),$_.hanxin_noblk,$a([46,20,30]),$a([7,19,28]),$a([1,16,28])]),$a(["69",159,18,8,21993,$a([19,116,22]),$a([1,105,22]),$_.hanxin_noblk,$a([20,70,30]),$a([7,66,28]),$a([1,63,28]),$a([40,35,30]),$a([1,42,32]),$a([1,43,32]),$a([54,20,30]),$a([1,19,30]),$_.hanxin_noblk]),$a(["70",161,18,8,22593,$a([17,126,24]),$a([2,115,22]),$_.hanxin_noblk,$a([24,70,30]),$a([4,74,32]),$_.hanxin_noblk,$a([48,31,26]),$a([2,18,26]),$_.hanxin_noblk,$a([54,19,28]),$a([6,15,26]),$a([1,14,26])]),$a(["71",163,18,8,23201,$a([29,84,16]),$_.hanxin_noblk,$_.hanxin_noblk,$a([29,70,30]),$_.hanxin_noblk,$_.hanxin_noblk,$a([6,34,30]),$a([3,36,30]),$a([38,33,28]),$a([58,20,30]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["72",165,18,8,23817,$a([16,147,28]),$a([1,149,28]),$_.hanxin_noblk,$a([31,66,28]),$a([1,37,26]),$_.hanxin_noblk,$a([48,33,28]),$a([1,23,26]),$_.hanxin_noblk,$a([53,20,30]),$a([6,19,28]),$a([1,17,28])]),$a(["73",167,19,8,24453,$a([20,115,22]),$a([2,134,24]),$_.hanxin_noblk,$a([29,66,28]),$a([2,56,26]),$a([2,57,26]),$a([45,36,30]),$a([2,15,28]),$_.hanxin_noblk,$a([59,20,30]),$a([2,21,32]),$_.hanxin_noblk]),$a(["74",169,19,8,25085,$a([17,147,28]),$a([1,134,26]),$_.hanxin_noblk,$a([26,70,30]),$a([5,75,32]),$_.hanxin_noblk,$a([47,35,30]),$a([1,48,32]),$_.hanxin_noblk,$a([64,18,28]),$a([2,33,30]),$a([1,35,30])]),$a(["75",171,17,9,25373,$a([22,115,22]),$a([1,133,24]),$_.hanxin_noblk,$a([33,65,28]),$a([1,74,28]),$_.hanxin_noblk,$a([43,36,30]),$a([5,27,28]),$a([1,30,28]),$a([57,20,30]),$a([5,21,32]),$a([1,24,32])]),$a(["76",173,17,9,26021,$a([18,136,26]),$a([2,142,26]),$_.hanxin_noblk,$a([33,66,28]),$a([2,49,26]),$_.hanxin_noblk,$a([48,35,30]),$a([2,38,28]),$_.hanxin_noblk,$a([64,20,30]),$a([1,20,32]),$_.hanxin_noblk]),$a(["77",175,17,9,26677,$a([19,126,24]),$a([2,135,26]),$a([1,136,26]),$a([32,66,28]),$a([2,55,26]),$a([2,56,26]),$a([49,36,30]),$a([2,18,32]),$_.hanxin_noblk,$a([65,18,28]),$a([5,27,30]),$a([1,29,30])]),$a(["78",177,18,9,27335,$a([20,137,26]),$a([1,130,26]),$_.hanxin_noblk,$a([30,75,32]),$a([2,71,32]),$_.hanxin_noblk,$a([46,35,30]),$a([6,39,32]),$_.hanxin_noblk,$a([3,12,30]),$a([70,19,28]),$_.hanxin_noblk]),$a(["79",179,18,9,28007,$a([20,147,28]),$_.hanxin_noblk,$_.hanxin_noblk,$a([35,70,30]),$_.hanxin_noblk,$_.hanxin_noblk,$a([49,35,30]),$a([5,35,28]),$_.hanxin_noblk,$a([70,20,30]),$_.hanxin_noblk,$_.hanxin_noblk]),$a(["80",181,18,9,28687,$a([21,136,26]),$a([1,155,28]),$_.hanxin_noblk,$a([34,70,30]),$a([1,64,28]),$a([1,65,28]),$a([54,35,30]),$a([1,45,30]),$_.hanxin_noblk,$a([68,20,30]),$a([3,18,28]),$a([1,19,28])]),$a(["81",183,18,9,29375,$a([19,126,24]),$a([5,115,22]),$a([1,114,22]),$a([33,70,30]),$a([3,65,28]),$a([1,64,28]),$a([52,35,30]),$a([3,41,32]),$a([1,40,32]),$a([67,20,30]),$a([5,21,32]),$a([1,24,32])]),$a(["82",185,18,9,30071,$a([2,150,28]),$a([21,136,26]),$_.hanxin_noblk,$a([32,70,30]),$a([6,65,28]),$_.hanxin_noblk,$a([52,38,32]),$a([2,27,32]),$_.hanxin_noblk,$a([73,20,30]),$a([2,22,32]),$_.hanxin_noblk]),$a(["83",187,17,10,30387,$a([21,126,24]),$a([4,136,26]),$_.hanxin_noblk,$a([30,74,32]),$a([6,73,30]),$_.hanxin_noblk,$a([54,35,30]),$a([4,40,32]),$_.hanxin_noblk,$a([75,20,30]),$a([1,20,28]),$_.hanxin_noblk]),$a(["84",189,17,10,31091,$a([30,105,20]),$a([1,114,22]),$_.hanxin_noblk,$a([3,45,22]),$a([55,47,20]),$_.hanxin_noblk,$a([2,26,26]),$a([62,33,28]),$_.hanxin_noblk,$a([79,18,28]),$a([4,33,30]),$_.hanxin_noblk])]);$_.hanxin_fpat=$a([$a([1,1,1,1,1,1,1,0]),$a([1,0,0,0,0,0,0,0]),$a([1,0,1,1,1,1,1,0]),$a([1,0,1,0,0,0,0,0]),$a([1,0,1,0,1,1,1,0]),$a([1,0,1,0,1,1,1,0]),$a([1,0,1,0,1,1,1,0]),$a([0,0,0,0,0,0,0,0])]);$_.hanxin_fpat2=$a([$a([1,1,1,0,1,0,1,0]),$a([1,1,1,0,1,0,1,0]),$a([1,1,1,0,1,0,1,0]),$a([0,0,0,0,1,0,1,0]),$a([1,1,1,1,1,0,1,0]),$a([0,0,0,0,0,0,1,0]),$a([1,1,1,1,1,1,1,0]),$a([0,0,0,0,0,0,0,0])]);var _Cq=$a([function(){$k[$j-1]=0;$k[$j++]=8},function(){var _Cp=$k[--$j];$k[$j++]=_Cp-1;$k[$j++]=_Cp-9}]);var _Cs=$a([function(){$k[$j-1]=1;$k[$j++]=8},function(){var _Cr=$k[--$j];$k[$j++]=_Cr-2;$k[$j++]=_Cr-9}]);var _Cu=$a([function(){$k[$j-1]=2;$k[$j++]=8},function(){var _Ct=$k[--$j];$k[$j++]=_Ct-3;$k[$j++]=_Ct-9}]);var _Cw=$a([function(){$k[$j-1]=3;$k[$j++]=8},function(){var _Cv=$k[--$j];$k[$j++]=_Cv-4;$k[$j++]=_Cv-9}]);var _Cy=$a([function(){$k[$j-1]=4;$k[$j++]=8},function(){var _Cx=$k[--$j];$k[$j++]=_Cx-5;$k[$j++]=_Cx-9}]);var _D0=$a([function(){$k[$j-1]=5;$k[$j++]=8},function(){var _Cz=$k[--$j];$k[$j++]=_Cz-6;$k[$j++]=_Cz-9}]);var _D2=$a([function(){$k[$j-1]=6;$k[$j++]=8},function(){var _D1=$k[--$j];$k[$j++]=_D1-7;$k[$j++]=_D1-9}]);var _D4=$a([function(){$k[$j-1]=7;$k[$j++]=8},function(){var _D3=$k[--$j];$k[$j++]=_D3-8;$k[$j++]=_D3-9}]);var _D6=$a([function(){$k[$j-1]=8;$k[$j++]=8},function(){var _D5=$k[--$j];$k[$j++]=_D5-9;$k[$j++]=_D5-9}]);var _D8=$a([function(){$k[$j-1]=8;$k[$j++]=7},function(){var _D7=$k[--$j];$k[$j++]=_D7-9;$k[$j++]=_D7-8}]);var _DA=$a([function(){$k[$j-1]=8;$k[$j++]=6},function(){var _D9=$k[--$j];$k[$j++]=_D9-9;$k[$j++]=_D9-7}]);var _DC=$a([function(){$k[$j-1]=8;$k[$j++]=5},function(){var _DB=$k[--$j];$k[$j++]=_DB-9;$k[$j++]=_DB-6}]);var _DE=$a([function(){$k[$j-1]=8;$k[$j++]=4},function(){var _DD=$k[--$j];$k[$j++]=_DD-9;$k[$j++]=_DD-5}]);var _DG=$a([function(){$k[$j-1]=8;$k[$j++]=3},function(){var _DF=$k[--$j];$k[$j++]=_DF-9;$k[$j++]=_DF-4}]);var _DI=$a([function(){$k[$j-1]=8;$k[$j++]=2},function(){var _DH=$k[--$j];$k[$j++]=_DH-9;$k[$j++]=_DH-3}]);var _DK=$a([function(){$k[$j-1]=8;$k[$j++]=1},function(){var _DJ=$k[--$j];$k[$j++]=_DJ-9;$k[$j++]=_DJ-2}]);var _DM=$a([function(){$k[$j-1]=8;$k[$j++]=0},function(){var _DL=$k[--$j];$k[$j++]=_DL-9;$k[$j++]=_DL-1}]);var _DP=$a([function(){$k[$j-1]-=9;$k[$j++]=0},function(){var _DO=$k[--$j];$k[$j++]=8;$k[$j++]=_DO-1}]);var _DS=$a([function(){$k[$j-1]-=9;$k[$j++]=1},function(){var _DR=$k[--$j];$k[$j++]=8;$k[$j++]=_DR-2}]);var _DV=$a([function(){$k[$j-1]-=9;$k[$j++]=2},function(){var _DU=$k[--$j];$k[$j++]=8;$k[$j++]=_DU-3}]);var _DY=$a([function(){$k[$j-1]-=9;$k[$j++]=3},function(){var _DX=$k[--$j];$k[$j++]=8;$k[$j++]=_DX-4}]);var _Db=$a([function(){$k[$j-1]-=9;$k[$j++]=4},function(){var _Da=$k[--$j];$k[$j++]=8;$k[$j++]=_Da-5}]);var _De=$a([function(){$k[$j-1]-=9;$k[$j++]=5},function(){var _Dd=$k[--$j];$k[$j++]=8;$k[$j++]=_Dd-6}]);var _Dh=$a([function(){$k[$j-1]-=9;$k[$j++]=6},function(){var _Dg=$k[--$j];$k[$j++]=8;$k[$j++]=_Dg-7}]);var _Dk=$a([function(){$k[$j-1]-=9;$k[$j++]=7},function(){var _Dj=$k[--$j];$k[$j++]=8;$k[$j++]=_Dj-8}]);var _Dn=$a([function(){$k[$j-1]-=9;$k[$j++]=8},function(){var _Dm=$k[--$j];$k[$j++]=8;$k[$j++]=_Dm-9}]);var _Dq=$a([function(){$k[$j-1]-=8;$k[$j++]=8},function(){var _Dp=$k[--$j];$k[$j++]=7;$k[$j++]=_Dp-9}]);var _Dt=$a([function(){$k[$j-1]-=7;$k[$j++]=8},function(){var _Ds=$k[--$j];$k[$j++]=6;$k[$j++]=_Ds-9}]);var _Dw=$a([function(){$k[$j-1]-=6;$k[$j++]=8},function(){var _Dv=$k[--$j];$k[$j++]=5;$k[$j++]=_Dv-9}]);var _Dz=$a([function(){$k[$j-1]-=5;$k[$j++]=8},function(){var _Dy=$k[--$j];$k[$j++]=4;$k[$j++]=_Dy-9}]);var _E2=$a([function(){$k[$j-1]-=4;$k[$j++]=8},function(){var _E1=$k[--$j];$k[$j++]=3;$k[$j++]=_E1-9}]);var _E5=$a([function(){$k[$j-1]-=3;$k[$j++]=8},function(){var _E4=$k[--$j];$k[$j++]=2;$k[$j++]=_E4-9}]);var _E8=$a([function(){$k[$j-1]-=2;$k[$j++]=8},function(){var _E7=$k[--$j];$k[$j++]=1;$k[$j++]=_E7-9}]);var _EB=$a([function(){$k[$j-1]-=1;$k[$j++]=8},function(){var _EA=$k[--$j];$k[$j++]=0;$k[$j++]=_EA-9}]);$_.hanxin_funmap=$a([_Cq,_Cs,_Cu,_Cw,_Cy,_D0,_D2,_D4,_D6,_D8,_DA,_DC,_DE,_DG,_DI,_DK,_DM,_DP,_DS,_DV,_DY,_Db,_De,_Dh,_Dk,_Dn,_Dq,_Dt,_Dw,_Dz,_E2,_E5,_E8,_EB]);$_.hanxin_rsparams=$a([$a([]),$a([]),$a([]),$a([]),$a([16,19]),$a([]),$a([]),$a([]),$a([256,355])]);var _ET=$a([function(){$j-=2;$k[$j++]=1},function(){var _EN=$k[--$j];var _EO=$k[--$j];$k[$j++]=$f(_EO+_EN)%2},function(){var _EP=$k[--$j];var _EQ=$k[--$j];$k[$j++]=$f((_EP+_EQ)%3+_EQ%3)%2},function(){var _ER=$k[--$j];var _ES=$k[--$j];$k[$j++]=$f(_ES%_ER+(_ER%_ES+(_ER%3+_ES%3)))%2}]);$_.hanxin_maskfuncs=_ET;$k[$j++]=Infinity;for(var _EU=0,_EV=$_.hanxin_rsparams.length;_EU<_EV;_EU++){var _EW=$g($_.hanxin_rsparams,_EU);$k[$j++]=_EW;if(_EW.length!=0){var _EX=$k[--$j];$q(_EX);var _EY=$k[--$j];$_.rspm=_EY;var _EZ=$k[--$j];$_.rsgf=_EZ;$k[$j++]=Infinity;$k[$j++]=1;for(var _Eb=0,_Ec=$_.rsgf-1;_Eb<_Ec;_Eb++){var _Ed=$k[$j-1];var _Ee=_Ed*2;$k[$j++]=_Ee;if(_Ee>=$_.rsgf){var _Eh=$k[--$j];$k[$j++]=$xo(_Eh,$_.rspm)}}$_.rsalog=$a();$_.rslog=$a($_.rsgf);for(var _Em=1,_El=$_.rsgf-1;_Em<=_El;_Em+=1){$p($_.rslog,$g($_.rsalog,_Em),_Em)}$k[$j++]=$a([$_.rslog,$_.rsalog])}else{$k[$j-1]=$a([])}}var _Eu=$a();$_.rstables=_Eu;$_.hanxin_rsprod=function(){var _Ev=$k[$j-1];if(_Ev!=0){var _Ex=$k[$j-2];if(_Ex!=0){var _Ez=$k[--$j];var _F1=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_Ez)+$g($_.rslog,_F1))%$_.rsgf_1)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.hanxin_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _F8=0,_F9=$_.rsnc;_F8<_F9;_F8++){$k[$j++]=0}$_.coeffs=$a();for(var _FC=1,_FB=$_.rsnc;_FC<=_FB;_FC+=1){var _FD=$_.coeffs;$p(_FD,_FC,$g(_FD,_FC-1));$k[$j++]=$g($_.rsalog,_FC);for(var _FH=_FC-1;_FH>=1;_FH-=1){var _FI=$_.coeffs;var _FK=$k[$j-1];$k[$j++]=_FI;$k[$j++]=_FH;$k[$j++]=$g(_FI,_FH);$k[$j++]=_FK;$_.hanxin_rsprod();var _FM=$k[--$j];var _FN=$k[--$j];var _FP=$k[--$j];$p(_FP,_FN,$xo(_FM,$g($_.coeffs,_FN-1)))}var _FQ=$_.coeffs;var _FS=$k[$j-1];$k[$j++]=_FQ;$k[$j++]=0;$k[$j++]=$g(_FQ,0);$k[$j++]=_FS;$_.hanxin_rsprod();var _FT=$k[--$j];var _FU=$k[--$j];var _FV=$k[--$j];$p(_FV,_FU,_FT);$j--}$k[$j++]=$G($_.coeffs,0,$_.coeffs.length-1)};$_.hanxin_coeffscachemax=15;$_.hanxin_coeffscachelimit=264;$k[$j++]=$_.hanxin_coeffscachemax;$k[$j++]=$_.hanxin_coeffscachelimit;bwipp_fifocache();var _Fb=$k[--$j];$_.hanxin_coeffscache=_Fb;$_.hanxin_coeffscachefetch=function(){var _Fc=$k[--$j];$_.rsbt=_Fc;var _Fd=$k[--$j];$_.rsnc=_Fd;$q($g($_.rstables,$_.rsbt));var _Fh=$k[--$j];$_.rsalog=_Fh;var _Fi=$k[--$j];$_.rslog=_Fi;$_.rsgf_1=$_.rsalog.length-1;var _Fm=$g($_.hanxin_coeffscache,"fetch");$k[$j++]=$f($_.rsbt*1e4+$_.rsnc);$k[$j++]=function(){$_.hanxin_gencoeffs()};$k[$j++]=function(){var _Fn=$k[--$j];$k[$j++]=_Fn.length};if(_Fm()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hanxin.globals);$_.dontdraw=false;$_.version="unset";$_.eclevel="L2";$_.parse=false;$_.parsefnc=false;$_.mask=-1;bwipp_processoptions();var _Fo=$k[--$j];$_.options=_Fo;var _Fp=$k[--$j];$_.barcode=_Fp;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.barcode.length==0){$k[$j++]="bwipp.hanxinEmptyData#32517";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>14e3){$k[$j++]="bwipp.hanxinInputTooLong#32521";$k[$j++]="The input data is too long";bwipp_raiseerror()}if($ne($_.version,"unset")){var _Fw=$_.version.length;var _Fx=_Fw>=1&&_Fw<=2;$k[$j++]=_Fx;if(_Fx){$j--;var _Fy=$_.version;$k[$j++]=true;for(var _Fz=0,_G0=_Fy.length;_Fz<_G0;_Fz++){var _G1=$g(_Fy,_Fz);if(_G1<48||_G1>57){$k[$j-1]=false}}var _G2=$k[$j-1];if(_G2){$j--;var _G4=$i($_.version);$k[$j++]=_G4>=1&&_G4<=84}}var _G5=$k[--$j];if(!_G5){$k[$j++]="bwipp.hanxinInvalidVersion#32532";$k[$j++]="Valid versions are 1 to 84";bwipp_raiseerror()}}var _G7=$_.eclevel.length==2;$k[$j++]=_G7;if(_G7){$j--;var _GA=$eq($G($_.eclevel,0,1),"L");$k[$j++]=_GA;if(_GA){$j--;var _GD=$g($_.eclevel,1)-48;$k[$j++]=_GD>=1&&_GD<=4}}var _GE=$k[--$j];if(!_GE){$k[$j++]="bwipp.hanxinInvalidErrorCorrectionLevel#32542";$k[$j++]="Valid error correction levels are L1 to L4";bwipp_raiseerror()}if($_.mask!=-1){if($_.mask<1||$_.mask>4){$k[$j++]="bwipp.hanxinInvalidErrorCorrectionLevel#32547";$k[$j++]="The valid masks are 1 to 4";bwipp_raiseerror()}}$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _GM=$k[--$j];$_.msg=_GM;$_.msglen=$_.msg.length;$_.tobin=function(){var _GO=$k[--$j];var _GP=$s(_GO);$k[$j++]=_GP;for(var _GR=0,_GQ=_GP.length-1;_GR<=_GQ;_GR+=1){var _GS=$k[$j-1];$p(_GS,_GR,48)}var _GT=$k[$j-1];$k[$j++]=_GT;var _GU=$k[$j-3];var _GV=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _GX=$R($s(_GV.length),_GU,2);$P(_GV,_GV.length-_GX.length,_GX)};$k[$j++]=4+13+$_.msglen*8;try{var _GZ=$k[--$j];$k[$j++]=$s(_GZ);var _Gb=false}catch(e){$k[$j++]=true}if(_Gb){$k[$j-1]="bwipp.hanxinInputTooLarge#32579";$k[$j++]="The input data exceeds the implementation limits";bwipp_raiseerror()}var _Gc=$k[--$j];$_.bits=_Gc;$P($_.bits,0,"0011");if($_.msglen>=8192){$k[$j++]="bwipp.hanxinInputTooLarge#32583";$k[$j++]="The input data exceeds the maximum symbol capacity";bwipp_raiseerror()}$k[$j++]=$_.bits;$k[$j++]=4;$k[$j++]=$_.msglen;$k[$j++]=13;$_.tobin();var _Gh=$k[--$j];var _Gi=$k[--$j];var _Gj=$k[--$j];$P(_Gj,_Gi,_Gh);for(var _Gm=0,_Gl=$_.msglen-1;_Gm<=_Gl;_Gm+=1){$_.i=_Gm;$k[$j++]=$_.bits;$k[$j++]=17+$_.i*8;$k[$j++]=$g($_.msg,$_.i);$k[$j++]=8;$_.tobin();var _Gs=$k[--$j];var _Gt=$k[--$j];var _Gu=$k[--$j];$P(_Gu,_Gt,_Gs)}$_.eclval=$g($_.eclevel,1)-49;for(var _Gx=0,_Gy=$_.hanxin_metrics.length;_Gx<_Gy;_Gx++){$_.m=$g($_.hanxin_metrics,_Gx);$_.vers=$g($_.m,0);$_.size=$g($_.m,1);$_.alnk=$g($_.m,2);$_.alnn=$g($_.m,3);$_.alnr=$f($_.size-$_.alnk*$_.alnn);$_.nmod=$g($_.m,4);$_.ncws=~~($_.nmod/8);$_.rbit=$_.nmod%8;$_.ecbs=$G($_.m,5+$_.eclval*3,3);var _HI=$_.ecbs;var _HP=0;for(var _HJ=0,_HK=_HI.length;_HJ<_HK;_HJ++){var _HL=$g(_HI,_HJ);_HP=$f(_HP+$g(_HL,0)*$g(_HL,2))}$_.ecws=_HP;$_.dcws=$f($_.ncws-$_.ecws);$_.dmod=$_.dcws*8;$_.okay=true;if($ne($_.version,"unset")&&$ne($_.version,$_.vers)){$_.okay=false}if($_.bits.length>$_.dmod){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.hanxinNoValidSymbol#32616";$k[$j++]="Maximum length exceeded or invalid content";bwipp_raiseerror()}$_.version=$_.vers;$_.msgbits=$_.bits;$_.e1nb=$g($g($_.ecbs,0),0);$_.e2nb=$g($g($_.ecbs,1),0);$_.e3nb=$g($g($_.ecbs,2),0);$_.e1dcws=$g($g($_.ecbs,0),1);$_.e2dcws=$g($g($_.ecbs,1),1);$_.e3dcws=$g($g($_.ecbs,2),1);$_.e1ecws=$g($g($_.ecbs,0),2);$_.e2ecws=$g($g($_.ecbs,1),2);$_.e3ecws=$g($g($_.ecbs,2),2);$_.pad=$s($_.dmod);for(var _I8=0,_I7=$_.pad.length-1;_I8<=_I7;_I8+=1){$p($_.pad,_I8,48)}$P($_.pad,0,$_.msgbits);$_.cws=$a($_.dcws);for(var _IG=0,_IF=$_.cws.length-1;_IG<=_IF;_IG+=1){$_.c=_IG;$_.cwb=$G($_.pad,$_.c*8,8);$_.cw=0;for(var _IK=0;_IK<=7;_IK+=1){$_.i=_IK;$_.cw=$f($_.cw+$pow(2,8-$_.i-1)*($g($_.cwb,$_.i)-48))}$p($_.cws,$_.c,$_.cw)}$_.rscodes=function(){var _IT=$k[--$j];$_.rsbt=_IT;var _IU=$k[--$j];$_.rsnc=_IU;var _IV=$k[--$j];$_.rscws=_IV;$k[$j++]="coeffs";$k[$j++]=$_.rsnc;$k[$j++]=$_.rsbt;$_.hanxin_coeffscachefetch();var _IY=$k[--$j];var _IZ=$k[--$j];$_[_IZ]=_IY;$_.lfsr=$a($_.rsnc);var _Ic=$_.rscws;var _Id=$_.coeffs;var _Ie=$_.rsnc;var _If=$_.lfsr;$k[$j++]=function(){var _Ih=$k[--$j];$k[$j++]=$g($_.rscws,_Ih)};$k[$j++]=_Ic.length;$k[$j++]=_Id;$k[$j++]=_Ie;$k[$j++]=_If;$k[$j++]=$_.hanxin_rsprod;bwipp_rsecbinary();$k[$j++]=$_.lfsr};$_.dcwsb=$a($f($_.e1nb+$_.e2nb+$_.e3nb));$_.ecwsb=$a($_.dcwsb.length);$_.in=0;$_.out=0;for(var _Ir=0,_Is=$_.e1nb;_Ir<_Is;_Ir++){var _Iw=$G($_.cws,$_.in,$_.e1dcws);$p($_.dcwsb,$_.out,_Iw);$k[$j++]=_Iw;$k[$j++]=$_.e1ecws;$k[$j++]=8;$_.rscodes();$k[$j++]=$_.ecwsb;$k[$j++]=$_.out;var _J2=$k[$j-3];var _J3=$k[$j-1];var _J4=$k[$j-2];$j-=3;$p(_J4,_J3,_J2);$_.in=$f($_.in+$_.e1dcws);$_.out=$_.out+1}for(var _J9=0,_JA=$_.e2nb;_J9<_JA;_J9++){var _JE=$G($_.cws,$_.in,$_.e2dcws);$p($_.dcwsb,$_.out,_JE);$k[$j++]=_JE;$k[$j++]=$_.e2ecws;$k[$j++]=8;$_.rscodes();$k[$j++]=$_.ecwsb;$k[$j++]=$_.out;var _JK=$k[$j-3];var _JL=$k[$j-1];var _JM=$k[$j-2];$j-=3;$p(_JM,_JL,_JK);$_.in=$f($_.in+$_.e2dcws);$_.out=$_.out+1}for(var _JR=0,_JS=$_.e3nb;_JR<_JS;_JR++){var _JW=$G($_.cws,$_.in,$_.e3dcws);$p($_.dcwsb,$_.out,_JW);$k[$j++]=_JW;$k[$j++]=$_.e3ecws;$k[$j++]=8;$_.rscodes();$k[$j++]=$_.ecwsb;$k[$j++]=$_.out;var _Jc=$k[$j-3];var _Jd=$k[$j-1];var _Je=$k[$j-2];$j-=3;$p(_Je,_Jd,_Jc);$_.in=$f($_.in+$_.e3dcws);$_.out=$_.out+1}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#32697";$k[$j++]=Infinity;var _Jk=$_.ecwsb;for(var _Jl=0,_Jm=_Jk.length;_Jl<_Jm;_Jl++){$q($g(_Jk,_Jl))}var _Jo=$a();$k[$j++]=_Jo;bwipp_raiseerror()}$_.cws=$a($_.ncws);$_.cw=0;for(var _Jt=0,_Js=$_.dcwsb.length-1;_Jt<=_Js;_Jt+=1){$_.i=_Jt;var _Jw=$g($_.dcwsb,$_.i);$P($_.cws,$_.cw,_Jw);$_.cw=_Jw.length+$_.cw;var _K2=$g($_.ecwsb,$_.i);$P($_.cws,$_.cw,_K2);$_.cw=_K2.length+$_.cw}$k[$j++]=Infinity;var _K6=$_.ncws;var _K7=12;var _K8=_K6-1;if(_K6-1>12){var _=_K7;_K7=_K8;_K8=_}for(var _KA=0,_K9=_K8;_KA<=_K9;_KA+=1){for(var _KD=_KA,_KC=$_.ncws-1;_KD<=_KC;_KD+=13){$k[$j++]=_KD;if(_KD<$_.ncws){var _KG=$k[--$j];$k[$j++]=$g($_.cws,_KG)}else{$j--}}}$_.cws=$a();if($_.rbit>0){$_.pad=$a($_.cws.length+1);$P($_.pad,0,$_.cws);$p($_.pad,$_.pad.length-1,0);$_.cws=$_.pad}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#32733";$k[$j++]=$_.cws;bwipp_raiseerror()}$k[$j++]=Infinity;for(var _KV=0,_KW=$_.size*$_.size;_KV<_KW;_KV++){$k[$j++]=-1}$_.pixs=$a();$_.qmv=function(){var _KY=$k[--$j];var _KZ=$k[--$j];$k[$j++]=$f(_KZ+_KY*$_.size)};if($_.alnn!=0){$_.trmv=function(){var _Kc=$k[--$j];var _Ke=$k[--$j];$k[$j++]=$f(_Kc*$_.size+$_.size-1-_Ke)};$_.aplot=function(){var _Kf=$k[$j-2];var _Kg=$k[$j-3];var _Kh=$k[$j-1];$j-=3;$k[$j++]=_Kh;$k[$j++]=_Kg;$k[$j++]=_Kf;$k[$j++]=_Kh;$k[$j++]=_Kf;$k[$j++]=_Kg;$_.trmv();var _Kj=$k[--$j];$k[$j++]=$_.pixs;$k[$j++]=_Kj;var _Kk=$k[$j-3];var _Kl=$k[$j-1];var _Km=$k[$j-2];$j-=3;$p(_Km,_Kl,_Kk);$_.trmv();var _Ko=$k[--$j];$k[$j++]=$_.pixs;$k[$j++]=_Ko;var _Kp=$k[$j-3];var _Kq=$k[$j-1];var _Kr=$k[$j-2];$j-=3;$p(_Kr,_Kq,_Kp)};$_.i=0;$_.stag=0;for(;;){if($_.i>=$_.size){break}for(var _Kw=0,_Kv=$_.size-1;_Kw<=_Kv;_Kw+=1){$_.j=_Kw;var _LA=$f($_.j+$_.alnr)<$_.size?(~~($_.j/$_.alnk)+$_.stag)%2==0&&!($_.i==0&&$_.j<$_.alnk)||$_.j%$_.alnk==0:$f($_.alnn+$_.stag)%2==0;if(_LA){$k[$j++]=$_.j;$k[$j++]=$_.i;$k[$j++]=1;$_.aplot();var _LD=$_.j;var _LE=$_.i;$k[$j++]=_LD+1;$k[$j++]=_LE+1;if(_LE+1<$_.size&&_LD+1<$_.size){$k[$j++]=0;$_.aplot()}else{$j-=2}}}if($f($_.i+$_.alnr)==$_.size){$_.i=$f($_.i+$_.alnr)-1}else{$_.i=$f($_.i+$_.alnk)}$_.stag=1-$_.stag}for(var _LT=$_.alnk,_LU=$_.alnk,_LS=$_.size-2;_LU<0?_LT>=_LS:_LT<=_LS;_LT+=_LU){$_.i=_LT;if(~~($_.i/$_.alnk)%2!=0){$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=$_.i-1;$_.trmv();var _LZ=$k[--$j];var _La=$k[--$j];$p(_La,_LZ,0);$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=$_.i+1;$_.trmv();var _Ld=$k[--$j];var _Le=$k[--$j];$p(_Le,_Ld,0);$k[$j++]=$_.pixs;$k[$j++]=1;$k[$j++]=$_.i-1;$_.trmv();var _Lh=$k[--$j];var _Li=$k[--$j];$p(_Li,_Lh,0);$k[$j++]=$_.pixs;$k[$j++]=1;$k[$j++]=$_.i;$_.trmv();var _Ll=$k[--$j];var _Lm=$k[--$j];$p(_Lm,_Ll,0);$k[$j++]=$_.pixs;$k[$j++]=1;$k[$j++]=$_.i+1;$_.trmv();var _Lp=$k[--$j];var _Lq=$k[--$j];$p(_Lq,_Lp,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i-1;$k[$j++]=0;$_.trmv();var _Lt=$k[--$j];var _Lu=$k[--$j];$p(_Lu,_Lt,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i+1;$k[$j++]=0;$_.trmv();var _Lx=$k[--$j];var _Ly=$k[--$j];$p(_Ly,_Lx,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i-1;$k[$j++]=1;$_.trmv();var _M1=$k[--$j];var _M2=$k[--$j];$p(_M2,_M1,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=1;$_.trmv();var _M5=$k[--$j];var _M6=$k[--$j];$p(_M6,_M5,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i+1;$k[$j++]=1;$_.trmv();var _M9=$k[--$j];var _MA=$k[--$j];$p(_MA,_M9,0)}$k[$j++]=$_.pixs;$k[$j++]=$_.size-1;$k[$j++]=$_.i-1;$_.trmv();var _ME=$k[--$j];var _MF=$k[--$j];if($g(_MF,_ME)!=1){$k[$j++]=$_.pixs;$k[$j++]=$_.size-1;$k[$j++]=$_.i-1;$_.trmv();var _MK=$k[--$j];var _ML=$k[--$j];$p(_ML,_MK,0);$k[$j++]=$_.pixs;$k[$j++]=$_.size-2;$k[$j++]=$_.i-1;$_.trmv();var _MP=$k[--$j];var _MQ=$k[--$j];$p(_MQ,_MP,0);$k[$j++]=$_.pixs;$k[$j++]=$_.size-2;$k[$j++]=$_.i;$_.trmv();var _MU=$k[--$j];var _MV=$k[--$j];$p(_MV,_MU,0);$k[$j++]=$_.pixs;$k[$j++]=$_.size-2;$k[$j++]=$_.i+1;$_.trmv();var _MZ=$k[--$j];var _Ma=$k[--$j];$p(_Ma,_MZ,0);$k[$j++]=$_.pixs;$k[$j++]=$_.size-1;$k[$j++]=$_.i+1;$_.trmv();var _Me=$k[--$j];var _Mf=$k[--$j];$p(_Mf,_Me,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i-1;$k[$j++]=$_.size-1;$_.trmv();var _Mj=$k[--$j];var _Mk=$k[--$j];$p(_Mk,_Mj,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i-1;$k[$j++]=$_.size-2;$_.trmv();var _Mo=$k[--$j];var _Mp=$k[--$j];$p(_Mp,_Mo,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.size-2;$_.trmv();var _Mt=$k[--$j];var _Mu=$k[--$j];$p(_Mu,_Mt,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i+1;$k[$j++]=$_.size-2;$_.trmv();var _My=$k[--$j];var _Mz=$k[--$j];$p(_Mz,_My,0);$k[$j++]=$_.pixs;$k[$j++]=$_.i+1;$k[$j++]=$_.size-1;$_.trmv();var _N3=$k[--$j];var _N4=$k[--$j];$p(_N4,_N3,0)}}}for(var _N6=0,_N5=$_.hanxin_fpat.length-1;_N6<=_N5;_N6+=1){$_.y=_N6;for(var _N9=0,_N8=$g($_.hanxin_fpat,0).length-1;_N9<=_N8;_N9+=1){$_.x=_N9;$_.fpb=$g($g($_.hanxin_fpat,$_.y),$_.x);$_.fpb2=$g($g($_.hanxin_fpat2,$_.y),$_.x);$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.qmv();var _NM=$k[--$j];var _NN=$k[--$j];$p(_NN,_NM,$_.fpb);$k[$j++]=$_.pixs;$k[$j++]=$f($_.size-$_.x)-1;$k[$j++]=$_.y;$_.qmv();var _NT=$k[--$j];var _NU=$k[--$j];$p(_NU,_NT,$_.fpb);$k[$j++]=$_.pixs;$k[$j++]=$f($_.size-$_.x)-1;$k[$j++]=$f($_.size-$_.y)-1;$_.qmv();var _Nb=$k[--$j];var _Nc=$k[--$j];$p(_Nc,_Nb,$_.fpb);$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$f($_.size-$_.y)-1;$_.qmv();var _Ni=$k[--$j];var _Nj=$k[--$j];$p(_Nj,_Ni,$_.fpb2)}}$k[$j++]=Infinity;for(var _Nk=0,_Nl=$_.hanxin_funmap.length;_Nk<_Nl;_Nk++){var _No=$g($_.hanxin_funmap,_Nk);$k[$j++]=Infinity;$F(_No,function(){var _Nq=$k[--$j];$k[$j++]=$_.size;if(_Nq()===true){return true}$as($a(2))});var _Ns=$a();$k[$j++]=_Ns}$_.functionmap=$a();var _Nt=$_.functionmap;for(var _Nu=0,_Nv=_Nt.length;_Nu<_Nv;_Nu++){$F($g(_Nt,_Nu),function(){var _Nx=$k[--$j];$q(_Nx);$_.qmv();var _Nz=$k[--$j];$p($_.pixs,_Nz,0)})}$_.maskfuncs=$_.hanxin_maskfuncs;if($_.mask!=-1){$_.maskfuncs=$a([$g($_.hanxin_maskfuncs,$_.mask-1)]);$_.bestmaskval=$_.mask-1}$_.masks=$a($_.maskfuncs.length);for(var _O8=0,_O7=$_.masks.length-1;_O8<=_O7;_O8+=1){$_.m=_O8;$_.mask=$a($_.size*$_.size);for(var _OE=0,_OD=$_.size-1;_OE<=_OD;_OE+=1){$_.j=_OE;for(var _OH=0,_OG=$_.size-1;_OH<=_OG;_OH+=1){$_.i=_OH;$k[$j++]=$_.i+1;$k[$j++]=$_.j+1;if($g($_.maskfuncs,$_.m)()===true){break}var _ON=$k[--$j];$k[$j++]=_ON==0;$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.j;$_.qmv();var _OR=$k[--$j];var _OS=$k[--$j];var _OU=$k[--$j];$k[$j++]=_OU&&$g(_OS,_OR)==-1?1:0;$k[$j++]=$_.mask;$k[$j++]=$_.i;$k[$j++]=$_.j;$_.qmv();var _OZ=$k[$j-3];var _Oa=$k[$j-1];var _Ob=$k[$j-2];$j-=3;$p(_Ob,_Oa,_OZ)}}$p($_.masks,$_.m,$_.mask)}$_.posx=0;$_.posy=0;$_.num=0;for(;;){if($_.posy==$_.size){break}$k[$j++]=$_.pixs;$k[$j++]=$_.posx;$k[$j++]=$_.posy;$_.qmv();var _Ok=$k[--$j];var _Ol=$k[--$j];if($g(_Ol,_Ok)==-1){$k[$j++]=$bs($g($_.cws,~~($_.num/8)),-(7-$_.num%8))&1;$k[$j++]=$_.pixs;$k[$j++]=$_.posx;$k[$j++]=$_.posy;$_.qmv();var _Ou=$k[$j-3];var _Ov=$k[$j-1];var _Ow=$k[$j-2];$j-=3;$p(_Ow,_Ov,_Ou);$_.num=$_.num+1}$_.posx=$_.posx+1;if($_.posx==$_.size){$_.posx=0;$_.posy=$_.posy+1}}$_.evalfulln1n3=function(){var _P2=$k[--$j];$_.scrle=_P2;$k[$j++]="scr1";$k[$j++]=0;$F($_.scrle,function(){var _P4=$k[$j-1];if(_P4>=3){var _P5=$k[--$j];var _P6=$k[--$j];var _P7=$f(_P6+_P5*4);$k[$j++]=_P7;$k[$j++]=_P7}$j--});var _P8=$k[--$j];var _P9=$k[--$j];$_[_P9]=_P8;$_.scr3=0;for(var _PC=5,_PB=$_.scrle.length-1;_PC<=_PB;_PC+=2){$_.j=_PC;if($g($_.scrle,$_.j)%3==0){$_.fact=~~($g($_.scrle,$_.j)/3);var _PL=$G($_.scrle,$_.j-4,4);for(var _PM=0,_PN=_PL.length;_PM<_PN;_PM++){$k[$j++]=$g(_PL,_PM)==$_.fact}var _PQ=$k[--$j];var _PR=$k[--$j];var _PS=$k[--$j];var _PT=$k[--$j];if(_PT&&(_PS&&(_PR&&_PQ))){if($_.j==5||$_.j+2>=$_.scrle.length){$_.scr3=$_.scr3+50}else{if($g($_.scrle,$_.j-5)>=3||$g($_.scrle,$_.j+1)>=3){$_.scr3=$_.scr3+50}}}}}for(var _Ph=1,_Pg=$_.scrle.length-5;_Ph<=_Pg;_Ph+=2){$_.j=_Ph;if($g($_.scrle,$_.j)%3==0){$_.fact=~~($g($_.scrle,$_.j)/3);var _Pq=$G($_.scrle,$_.j+1,4);for(var _Pr=0,_Ps=_Pq.length;_Pr<_Ps;_Pr++){$k[$j++]=$g(_Pq,_Pr)==$_.fact}var _Pv=$k[--$j];var _Pw=$k[--$j];var _Px=$k[--$j];var _Py=$k[--$j];if(_Py&&(_Px&&(_Pw&&_Pv))){if($_.j==1||$_.j+6>=$_.scrle.length){$_.scr3=$_.scr3+50}else{if($g($_.scrle,$_.j-1)>=3||$g($_.scrle,$_.j+5)>=3){$_.scr3=$_.scr3+50}}}}}$k[$j++]=$_.scr1;$k[$j++]=$_.scr3};$_.evalfull=function(){var _QC=$k[--$j];$_.sym=_QC;$_.n1=0;$_.n3=0;$_.rle=$a($_.size+1);$_.lastpairs=$a($_.size);$_.thispairs=$a($_.size);$_.sizeadd1=$_.size+1;for(var _QM=0,_QL=$_.size-1;_QM<=_QL;_QM+=1){$_.i=_QM;$k[$j++]=Infinity;var _QO=$_.size;$k[$j++]=0;$k[$j++]=0;for(var _QQ=$_.i,_QR=_QO,_QP=_QO*_QO-1;_QR<0?_QQ>=_QP:_QQ<=_QP;_QQ+=_QR){var _QT=$g($_.sym,_QQ);var _QU=$k[--$j];$k[$j++]=_QT;if($eq(_QU,_QT)){var _QV=$k[--$j];$k[$j-1]+=1;$k[$j++]=_QV}else{var _QX=$k[--$j];$k[$j++]=1;$k[$j++]=_QX}}$j--;var _QZ=$m()+2;$as($G($_.rle,0,_QZ-2));$_.evalfulln1n3();var _Qc=$k[--$j];$_.n3=$f(_Qc+$_.n3);var _Qe=$k[--$j];$_.n1=$f(_Qe+$_.n1);$j--;$_.symrow=$G($_.sym,$_.i*$_.size,$_.size);$k[$j++]=Infinity;var _Qk=$_.symrow;$k[$j++]=0;$k[$j++]=0;for(var _Ql=0,_Qm=_Qk.length;_Ql<_Qm;_Ql++){var _Qn=$g(_Qk,_Ql);var _Qo=$k[--$j];$k[$j++]=_Qn;if($eq(_Qo,_Qn)){var _Qp=$k[--$j];$k[$j-1]+=1;$k[$j++]=_Qp}else{var _Qr=$k[--$j];$k[$j++]=1;$k[$j++]=_Qr}}$j--;var _Qt=$m()+2;$as($G($_.rle,0,_Qt-2));$_.evalfulln1n3();var _Qw=$k[--$j];$_.n3=$f(_Qw+$_.n3);var _Qy=$k[--$j];$_.n1=$f(_Qy+$_.n1);$j--}$k[$j++]=$f($_.n1+$_.n3)};$_.bestscore=999999999;for(var _R3=0,_R2=$_.masks.length-1;_R3<=_R2;_R3+=1){$_.m=_R3;$_.masksym=$a($_.size*$_.size);for(var _RA=0,_R9=$_.size*$_.size-1;_RA<=_R9;_RA+=1){$_.i=_RA;$p($_.masksym,$_.i,$xo($g($_.pixs,$_.i),$g($g($_.masks,$_.m),$_.i)))}if($_.masks.length!=1){$k[$j++]=$_.masksym;$_.evalfull();var _RN=$k[--$j];$_.score=_RN;if($_.score<$_.bestscore){$_.bestsym=$_.masksym;$_.bestmaskval=$_.m;$_.bestscore=$_.score}}else{$_.bestsym=$_.masksym}}$_.pixs=$_.bestsym;$_.funval=((~~(($_.size-21)/2)+20)*4+$_.eclval)*4+$_.bestmaskval;$_.fundata=$a([$flr(($_.funval&3840)/256),$flr(($_.funval&240)/16),$_.funval&15]);$k[$j++]=Infinity;var _Rb=$_.fundata;for(var _Rc=0,_Rd=_Rb.length;_Rc<_Rd;_Rc++){$k[$j++]=$g(_Rb,_Rc);$k[$j++]=4;$_.tobin();var _Rf=$k[--$j];$F(_Rf,function(){$k[$j-1]-=48})}$k[$j++]=$_.fundata;$k[$j++]=4;$k[$j++]=4;$_.rscodes();var _Ri=$k[--$j];$F(_Ri,function(){$k[$j++]=4;$_.tobin();var _Rj=$k[--$j];$F(_Rj,function(){$k[$j-1]-=48})});$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$k[$j++]=0;$k[$j++]=1;$_.funbits=$a();for(var _Rn=0,_Rm=$_.functionmap.length-1;_Rn<=_Rm;_Rn+=1){$_.i=_Rn;$F($g($_.functionmap,$_.i),function(){var _Rs=$k[--$j];$k[$j++]=$_.pixs;$q(_Rs);$_.qmv();var _Rw=$k[--$j];var _Rx=$k[--$j];$p(_Rx,_Rw,$g($_.funbits,$_.i))})}$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.size],["pixy",$_.size],["height",$_.size*$_.modunit/72],["width",$_.size*$_.modunit/72],["borderleft",3],["borderright",3],["bordertop",3],["borderbottom",3],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_dotcode(){if(!bwipp_dotcode.globals){var $__=$_;$_=bwipp_dotcode.globals={};$_.dotcode_laa=-1;$_.dotcode_lab=-2;$_.dotcode_lac=-3;$_.dotcode_bin=-4;$_.dotcode_sfa=-5;$_.dotcode_sfb=-6;$_.dotcode_sb2=-7;$_.dotcode_sb3=-8;$_.dotcode_sb4=-9;$_.dotcode_sb5=-10;$_.dotcode_sb6=-11;$_.dotcode_sfc=-12;$_.dotcode_sc2=-13;$_.dotcode_sc3=-14;$_.dotcode_sc4=-15;$_.dotcode_sc5=-16;$_.dotcode_sc6=-17;$_.dotcode_sc7=-18;$_.dotcode_bsa=-19;$_.dotcode_bsb=-20;$_.dotcode_tma=-21;$_.dotcode_tmb=-22;$_.dotcode_tmc=-23;$_.dotcode_tms=-24;$_.dotcode_fn1=-25;$_.dotcode_fn2=-26;$_.dotcode_fn3=-27;$_.dotcode_crl=-28;$_.dotcode_aim=-29;$_.dotcode_m05=-30;$_.dotcode_m06=-31;$_.dotcode_m12=-32;$_.dotcode_mac=-33;$_.laa=$_.dotcode_laa;$_.lab=$_.dotcode_lab;$_.lac=$_.dotcode_lac;$_.bin=$_.dotcode_bin;$_.sfa=$_.dotcode_sfa;$_.sfb=$_.dotcode_sfb;$_.sb2=$_.dotcode_sb2;$_.sb3=$_.dotcode_sb3;$_.sb4=$_.dotcode_sb4;$_.sb5=$_.dotcode_sb5;$_.sb6=$_.dotcode_sb6;$_.sfc=$_.dotcode_sfc;$_.sc2=$_.dotcode_sc2;$_.sc3=$_.dotcode_sc3;$_.sc4=$_.dotcode_sc4;$_.sc5=$_.dotcode_sc5;$_.sc6=$_.dotcode_sc6;$_.sc7=$_.dotcode_sc7;$_.bsa=$_.dotcode_bsa;$_.bsb=$_.dotcode_bsb;$_.tma=$_.dotcode_tma;$_.tmb=$_.dotcode_tmb;$_.tmc=$_.dotcode_tmc;$_.tms=$_.dotcode_tms;$_.fn1=$_.dotcode_fn1;$_.fn2=$_.dotcode_fn2;$_.fn3=$_.dotcode_fn3;$_.crl=$_.dotcode_crl;$_.aim=$_.dotcode_aim;$_.m05=$_.dotcode_m05;$_.m06=$_.dotcode_m06;$_.m12=$_.dotcode_m12;$_.mac=$_.dotcode_mac;$_.dotcode_charmaps=$a([$a([32,32,"00"]),$a(["!","!","01"]),$a(['"','"',"02"]),$a(["#","#","03"]),$a(["$","$","04"]),$a(["%","%","05"]),$a(["&","&","06"]),$a(["'","'","07"]),$a([40,40,"08"]),$a([41,41,"09"]),$a(["*","*","10"]),$a(["+","+","11"]),$a([",",",","12"]),$a(["-","-","13"]),$a([".",".","14"]),$a(["/","/","15"]),$a(["0","0","16"]),$a(["1","1","17"]),$a(["2","2","18"]),$a(["3","3","19"]),$a(["4","4","20"]),$a(["5","5","21"]),$a(["6","6","22"]),$a(["7","7","23"]),$a(["8","8","24"]),$a(["9","9","25"]),$a([":",":","26"]),$a([";",";","27"]),$a(["<","<","28"]),$a(["=","=","29"]),$a([">",">","30"]),$a(["?","?","31"]),$a(["@","@","32"]),$a(["A","A","33"]),$a(["B","B","34"]),$a(["C","C","35"]),$a(["D","D","36"]),$a(["E","E","37"]),$a(["F","F","38"]),$a(["G","G","39"]),$a(["H","H","40"]),$a(["I","I","41"]),$a(["J","J","42"]),$a(["K","K","43"]),$a(["L","L","44"]),$a(["M","M","45"]),$a(["N","N","46"]),$a(["O","O","47"]),$a(["P","P","48"]),$a(["Q","Q","49"]),$a(["R","R","50"]),$a(["S","S","51"]),$a(["T","T","52"]),$a(["U","U","53"]),$a(["V","V","54"]),$a(["W","W","55"]),$a(["X","X","56"]),$a(["Y","Y","57"]),$a(["Z","Z","58"]),$a(["[","[","59"]),$a([92,92,"60"]),$a(["]","]","61"]),$a(["^","^","62"]),$a(["_","_","63"]),$a([0,"`","64"]),$a([1,"a","65"]),$a([2,"b","66"]),$a([3,"c","67"]),$a([4,"d","68"]),$a([5,"e","69"]),$a([6,"f","70"]),$a([7,"g","71"]),$a([8,"h","72"]),$a([9,"i","73"]),$a([10,"j","74"]),$a([11,"k","75"]),$a([12,"l","76"]),$a([13,"m","77"]),$a([14,"n","78"]),$a([15,"o","79"]),$a([16,"p","80"]),$a([17,"q","81"]),$a([18,"r","82"]),$a([19,"s","83"]),$a([20,"t","84"]),$a([21,"u","85"]),$a([22,"v","86"]),$a([23,"w","87"]),$a([24,"x","88"]),$a([25,"y","89"]),$a([26,"z","90"]),$a([27,"{","91"]),$a([28,"|","92"]),$a([29,"}","93"]),$a([30,"~","94"]),$a([31,127,"95"]),$a([$_.sfb,$_.crl,"96"]),$a([$_.sb2,9,"97"]),$a([$_.sb3,28,"98"]),$a([$_.sb4,29,"99"]),$a([$_.sb5,30,$_.aim]),$a([$_.sb6,$_.sfa,$_.laa]),$a([$_.lab,$_.laa,$_.sfb]),$a([$_.sc2,$_.sc2,$_.sb2]),$a([$_.sc3,$_.sc3,$_.sb3]),$a([$_.sc4,$_.sc4,$_.sb4]),$a([$_.lac,$_.lac,$_.lab]),$a([$_.fn1,$_.fn1,$_.fn1]),$a([$_.fn2,$_.fn2,$_.fn2]),$a([$_.fn3,$_.fn3,$_.fn3]),$a([$_.bsa,$_.bsa,$_.bsa]),$a([$_.bsb,$_.bsb,$_.bsb]),$a([$_.bin,$_.bin,$_.bin])]);$_.dotcode_encs=$a(["101010101","010101011","010101101","010110101","011010101","101010110","101011010","101101010","110101010","010101110","010110110","010111010","011010110","011011010","011101010","100101011","100101101","100110101","101001011","101001101","101010011","101011001","101100101","101101001","110010101","110100101","110101001","001010111","001011011","001011101","001101011","001101101","001110101","010010111","010011011","010011101","010100111","010110011","010111001","011001011","011001101","011010011","011011001","011100101","011101001","100101110","100110110","100111010","101001110","101011100","101100110","101101100","101110010","101110100","110010110","110011010","110100110","110101100","110110010","110110100","111001010","111010010","111010100","001011110","001101110","001110110","001111010","010011110","010111100","011001110","011011100","011100110","011101100","011110010","011110100","100010111","100011011","100011101","100100111","100110011","100111001","101000111","101100011","101110001","110001011","110001101","110010011","110011001","110100011","110110001","111000101","111001001","111010001","000101111","000110111","000111011","000111101","001001111","001100111","001110011","001111001","010001111","011000111","011100011","011110001","100011110","100111100","101111000","110001110","110011100","110111000","111000110","111001100"]);$_.dotcode_encfuncs=$a(["encA","encB","encC","encBIN"]);$_.dotcode_maskvals=$a([0,3,7,17]);$_.dotcode_maskbits=$a(["00","01","10","11"]);$k[$j++]=Infinity;$k[$j++]=1;for(var _2b=0;_2b<112;_2b++){var _2c=$k[$j-1];$k[$j++]=_2c*3%113}var _2d=$a();$_.rsalog=_2d;$_.charvals=$a([new Map,new Map,new Map]);for(var _2f=0,_2e=$_.dotcode_charmaps.length-1;_2f<=_2e;_2f+=1){$_.i=_2f;$_.encs=$g($_.dotcode_charmaps,$_.i);for(var _2i=0;_2i<=2;_2i+=1){$_.j=_2i;var _2l=$g($_.encs,$_.j);var _2m=$t(_2l);if($eq(_2m,"stringtype")){_2l=$g(_2l,0)}$p($g($_.charvals,$_.j),_2l,$_.i)}}$_.Avals=$g($_.charvals,0);$_.Bvals=$g($_.charvals,1);$p($_.Bvals,$_.dotcode_m05,97);$p($_.Bvals,$_.dotcode_m06,98);$p($_.Bvals,$_.dotcode_m12,99);$p($_.Bvals,$_.dotcode_mac,100);$_.Bvals=$_.Bvals;$_.Cvals=$g($_.charvals,2);$_.charvals=$_.charvals;$k[$j++]=Infinity;var _36=$a([$_.dotcode_sc2,$_.dotcode_sc3,$_.dotcode_sc4,$_.dotcode_sc5,$_.dotcode_sc6,$_.dotcode_sc7,$_.dotcode_tma,$_.dotcode_tmb,$_.dotcode_tmc,$_.dotcode_tms]);$k[$j++]=102;for(var _37=0,_38=_36.length;_37<_38;_37++){var _3A=$k[--$j];var _3B=_3A+1;$k[$j++]=$g(_36,_37);$k[$j++]=_3B;$k[$j++]=_3B}$j--;var _3C=$d();$_.BINvals=_3C;$_.dotcode_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _3E=0,_3F=$_.NC;_3E<_3F;_3E++){$k[$j++]=0}$_.coeffs=$a();for(var _3I=1,_3H=$_.NC;_3I<=_3H;_3I+=1){$k[$j++]=_3I;for(var _3K=$_.NC;_3K>=1;_3K-=1){var _3L=$_.coeffs;var _3O=$k[$j-1];$p(_3L,_3K,$f($g(_3L,_3K)+113-$g($_.rsalog,_3O)*$g($_.coeffs,_3K-1)%113)%113)}$j--}for(var _3T=$_.NC;_3T>=1;_3T-=1){$k[$j++]=$g($_.coeffs,_3T)}$as($a($_.NC))};$_.dotcode_coeffscachemax=79;$_.dotcode_coeffscachelimit=3160;$k[$j++]=$_.dotcode_coeffscachemax;$k[$j++]=$_.dotcode_coeffscachelimit;bwipp_fifocache();var _3a=$k[--$j];$_.dotcode_coeffscache=_3a;$_.dotcode_coeffscachefetch=function(){var _3b=$k[--$j];$_.NC=_3b;var _3d=$g($_.dotcode_coeffscache,"fetch");$k[$j++]=$_.NC;$k[$j++]=function(){$_.dotcode_gencoeffs()};$k[$j++]=function(){var _3e=$k[--$j];$k[$j++]=_3e.length};if(_3d()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_dotcode.globals);$_.dontdraw=false;$_.rows=-1;$_.columns=-1;$_.ratio=-1;$_.parse=false;$_.parsefnc=false;$_.raw=false;$_.fast=false;$_.mask=-1;bwipp_processoptions();var _3f=$k[--$j];$_.options=_3f;var _3g=$k[--$j];$_.barcode=_3g;if($_.barcode.length>4e3){$k[$j++]="bwipp.dotcodeinputTooLong#33295";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.rows!=-1){if($_.rows<5){$k[$j++]="bwipp.dotcodeBadRows#33322";$k[$j++]="There must be at least 5 rows";bwipp_raiseerror()}}if($_.columns!=-1){if($_.columns<5){$k[$j++]="bwipp.dotcodeBadColumns#33328";$k[$j++]="There must be at least 5 columns";bwipp_raiseerror()}}if($_.rows!=-1&&$_.columns!=-1&&($_.rows+$_.columns)%2!=1){$k[$j++]="bwipp.dotcodeRowsColumnsBadParity#33333";$k[$j++]="Sum of rows and columns must be odd";bwipp_raiseerror()}if($_.ratio==-1&&$_.rows==-1&&$_.columns==-1){$_.ratio=3/2}if($_.ratio!=-1){if($_.ratio<=0){$k[$j++]="bwipp.dotcodeRatio#33344";$k[$j++]="The ratio must be greater than 0";bwipp_raiseerror()}}if($_.mask!=-1){if($_.mask<0||$_.mask>3){$k[$j++]="bwipp.dotcodeBadMask#33350";$k[$j++]="Valid mask values are 0 to 3";bwipp_raiseerror()}}$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["eci",true],["FNC1",$_.dotcode_fn1],["FNC3",$_.dotcode_fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _44=$k[--$j];$_.msg=_44;$_.msglen=$_.msg.length;$k[$j++]="numecis";$k[$j++]=0;$F($_.msg,function(){var _47=$k[--$j];if(_47<=-1e6){$k[$j-1]+=1}});var _49=$k[--$j];var _4A=$k[--$j];$_[_4A]=_49;$_.msgtmp=$a($f($_.msg.length+$_.numecis*6));$_.j=0;for(var _4G=0,_4F=$_.msg.length-1;_4G<=_4F;_4G+=1){var _4I=$g($_.msg,_4G);$k[$j++]=_4I;if(_4I<=-1e6){var _4J=$k[--$j];var _4M=$G($R($s(7),-_4J,10),1,6);for(var _4N=0,_4O=_4M.length;_4N<_4O;_4N++){$k[$j++]=$g(_4M,_4N)}$as($a(6));var _4S=$k[--$j];$P($_.msgtmp,$_.j+1,_4S);$p($_.msgtmp,$_.j,$_.dotcode_fn2);$_.j=$_.j+7}else{var _4Y=$k[--$j];$p($_.msgtmp,$_.j,_4Y);$_.j=$_.j+1}}$_.msg=$_.msgtmp;$_.msglen=$_.msg.length;$_.A=0;$_.B=1;$_.C=2;$_.BIN=3;$k[$j++]=Infinity;for(var _4e=0,_4f=$_.msglen+1;_4e<_4f;_4e++){$k[$j++]=0}$_.nDigits=$a();$k[$j++]=Infinity;for(var _4h=0,_4i=$_.msglen+1;_4h<_4i;_4h++){$k[$j++]=false}$_.SeventeenTen=$a();$k[$j++]=Infinity;for(var _4k=0,_4l=$_.msglen+1;_4k<_4l;_4k++){$k[$j++]=false}$_.ECI=$a();$k[$j++]=Infinity;for(var _4n=0,_4o=$_.msglen+1;_4n<_4o;_4n++){$k[$j++]=false}$_.DatumA=$a();$k[$j++]=Infinity;for(var _4q=0,_4r=$_.msglen+1;_4q<_4r;_4q++){$k[$j++]=false}$_.DatumB=$a();$k[$j++]=Infinity;for(var _4t=0,_4u=$_.msglen+1;_4t<_4u;_4t++){$k[$j++]=false}$_.DatumC=$a();$k[$j++]=Infinity;for(var _4w=0,_4x=$_.msglen+8;_4w<_4x;_4w++){$k[$j++]=false}$_.Binary=$a();$k[$j++]=Infinity;for(var _4z=0,_50=$_.msglen+1;_4z<_50;_4z++){$k[$j++]=0}$_.AheadC=$a();$k[$j++]=Infinity;for(var _52=0,_53=$_.msglen+1;_52<_53;_52++){$k[$j++]=0}$_.TryC=$a();$k[$j++]=Infinity;for(var _55=0,_56=$_.msglen+1;_55<_56;_55++){$k[$j++]=0}$_.AheadA=$a();$k[$j++]=Infinity;for(var _58=0,_59=$_.msglen+1;_58<_59;_58++){$k[$j++]=0}$_.AheadB=$a();$k[$j++]=Infinity;for(var _5B=0,_5C=$_.msglen+1;_5B<_5C;_5B++){$k[$j++]=0}$_.UntilEndSeg=$a();for(var _5E=$_.msglen-1;_5E>=0;_5E-=1){$_.i=_5E;$_.barchar=$g($_.msg,$_.i);if($_.barchar>=48&&$_.barchar<=57){$p($_.nDigits,$_.i,$g($_.nDigits,$_.i+1)+1)}if($has($_.Avals,$_.barchar)){$p($_.DatumA,$_.i,true)}if($has($_.Bvals,$_.barchar)){$p($_.DatumB,$_.i,true)}$_.CRLF=false;if($_.barchar==13&&$_.i<$_.msglen-1){if($g($_.msg,$_.i+1)==10){$_.CRLF=true}}if($_.CRLF){$p($_.DatumB,$_.i,true)}if($g($_.nDigits,$_.i)>=2){$p($_.DatumC,$_.i,true)}if($_.barchar<0){$p($_.DatumC,$_.i,true)}if($_.barchar>=128){$p($_.Binary,$_.i,true)}if($g($_.nDigits,$_.i)>=10){$k[$j++]=Infinity;$q($G($_.msg,$_.i,10));for(var _5z=0;_5z<1;_5z++){var _60=$k[--$j];if(_60!=48){$k[$j++]=false;break}var _61=$k[--$j];if(_61!=49){$k[$j++]=false;break}$j-=6;var _62=$k[--$j];if(_62!=55){$k[$j++]=false;break}var _63=$k[--$j];if(_63!=49){$k[$j++]=false;break}$k[$j++]=true}$k[$j++]=$_.SeventeenTen;$k[$j++]=$_.i;var _66=$k[$j-3];var _67=$k[$j-1];var _68=$k[$j-2];$j-=3;$p(_68,_67,_66);$l()}if($g($_.nDigits,$_.i+1)>=6&&$g($_.msg,$_.i)==$_.dotcode_fn2){$p($_.ECI,$_.i,true)}if($_.barchar<0&&$_.barchar!=$_.dotcode_fn3){$p($_.AheadC,$_.i,$g($_.AheadC,$_.i+1)+1)}else{if($g($_.nDigits,$_.i)<=1){$p($_.AheadC,$_.i,0)}else{$p($_.AheadC,$_.i,$g($_.AheadC,$_.i+2)+1)}}if($g($_.nDigits,$_.i)>0&&$gt($g($_.AheadC,$_.i),$g($_.AheadC,$_.i+1))){$p($_.TryC,$_.i,$g($_.AheadC,$_.i))}if($g($_.DatumA,$_.i)&&$g($_.TryC,$_.i)<2&&$_.barchar!=$_.dotcode_fn3){$p($_.AheadA,$_.i,$g($_.AheadA,$_.i+1)+1)}if($g($_.DatumB,$_.i)&&$g($_.TryC,$_.i)<2&&$_.barchar!=$_.dotcode_fn3){var _7F=$_.AheadB;var _7E=$_.i;var _7C=$_.AheadB;var _7B=$_.i+1;if($_.CRLF){_7B=_7B+1}$p(_7F,_7E,$g(_7C,_7B)+1)}if($_.barchar!=$_.dotcode_fn3){$p($_.UntilEndSeg,$_.i,$g($_.UntilEndSeg,$_.i+1)+1)}}$_.addtocws=function(){var _7M=$k[--$j];$P($_.cws,$_.j,_7M);$_.j=_7M.length+$_.j};$_.base259to103=function(){var _7Q=$k[--$j];$_.in=_7Q;$_.inlen=$_.in.length;$k[$j++]=Infinity;for(var _7T=0,_7U=5-$_.inlen;_7T<_7U;_7T++){$k[$j++]=0}$q($_.in);$_.in=$a();$_.out=$a(6);$k[$j++]=Infinity;$q($G($_.in,0,2));$_.msbs=$a();$k[$j++]=Infinity;$q($_.msbs);var _7a=$k[--$j];var _7b=$k[--$j];$k[$j++]=$f(_7a+_7b*259);for(var _7c=0;_7c<2;_7c++){var _7d=$k[--$j];$k[$j++]=_7d%103;$k[$j++]=~~(_7d/103)}$_.mscs=$a();$k[$j++]=Infinity;$q($G($_.in,2,3));$_.lsbs=$a();$k[$j++]=Infinity;$q($_.lsbs);var _7h=$k[--$j];var _7i=$k[--$j];var _7j=$k[--$j];$k[$j++]=$f(_7h+_7i*259+_7j*67081);for(var _7k=0;_7k<3;_7k++){var _7l=$k[--$j];$k[$j++]=_7l%103;$k[$j++]=~~(_7l/103)}$_.lscs=$a();var _7n=$g($_.lscs,0);var _7p=$g($_.mscs,0);$p($_.out,5,$f(_7n+_7p*42)%103);var _7s=$g($_.lscs,1);var _7u=$g($_.mscs,0);var _7w=$g($_.mscs,1);$p($_.out,4,$f(~~((_7n+_7p*42)/103)+_7s+_7u*68+_7w*42)%103);var _7z=$g($_.lscs,2);var _81=$g($_.mscs,0);var _83=$g($_.mscs,1);var _85=$g($_.mscs,2);$p($_.out,3,$f(~~((~~((_7n+_7p*42)/103)+_7s+_7u*68+_7w*42)/103)+_7z+_81*92+_83*68+_85*42)%103);var _88=$g($_.lscs,3);var _8A=$g($_.mscs,0);var _8C=$g($_.mscs,1);var _8E=$g($_.mscs,2);$p($_.out,2,$f(~~((~~((~~((_7n+_7p*42)/103)+_7s+_7u*68+_7w*42)/103)+_7z+_81*92+_83*68+_85*42)/103)+_88+_8A*15+_8C*92+_8E*68)%103);var _8H=$g($_.mscs,1);var _8J=$g($_.mscs,2);$p($_.out,1,$f(~~((~~((~~((~~((_7n+_7p*42)/103)+_7s+_7u*68+_7w*42)/103)+_7z+_81*92+_83*68+_85*42)/103)+_88+_8A*15+_8C*92+_8E*68)/103)+_8H*15+_8J*92)%103);$p($_.out,0,$f(~~((~~((~~((~~((~~((_7n+_7p*42)/103)+_7s+_7u*68+_7w*42)/103)+_7z+_81*92+_83*68+_85*42)/103)+_88+_8A*15+_8C*92+_8E*68)/103)+_8H*15+_8J*92)/103)+$g($_.mscs,2)*15)%103);$k[$j++]=$G($_.out,6-$_.inlen-1,$_.inlen+1)};$_.finaliseBIN=function(){if($_.bpos!=0){$k[$j++]=$G($_.bvals,0,$_.bpos);$_.base259to103();var _8W=$k[--$j];$q(_8W);$_.bpos=0}};$_.addtobin=function(){var _8Y=$k[--$j];$p($_.bvals,$_.bpos,_8Y);$_.bpos=$_.bpos+1;if($_.bpos==5){$_.finaliseBIN()}};$_.ECIabc=function(){var _8e=$G($_.msg,$_.i+1,6);var _8j=0;for(var _8f=0,_8g=_8e.length;_8f<_8g;_8f++){_8j=$f(_8j+($g(_8e,_8f)-48))*10}var _8k=~~(_8j/10);$k[$j++]=_8k;if(_8k>=40){var _8l=$k[--$j];var _8m=_8l-40;var _8n=_8m%12769;$k[$j++]=~~(_8m/12769)+40;$k[$j++]=~~(_8n/113);$k[$j++]=_8n%113}};$_.ECIbin=function(){$k[$j++]=Infinity;var _8q=$G($_.msg,$_.i+1,6);var _8v=0;for(var _8r=0,_8s=_8q.length;_8r<_8s;_8r++){_8v=$f(_8v+($g(_8q,_8r)-48))*10}var _8w=~~(_8v/10);$k[$j++]=_8w;if(_8w>=65536){var _8x=$k[--$j];var _8y=_8x%65536;$k[$j++]=258;$k[$j++]=~~(_8x/65536);$k[$j++]=~~(_8y/256);$k[$j++]=_8y%256}else{var _8z=$k[$j-1];if(_8z>=256){var _90=$k[--$j];$k[$j++]=257;$k[$j++]=~~(_90/256);$k[$j++]=_90%256}else{var _91=$k[--$j];$k[$j++]=256;$k[$j++]=_91}}var _92=$a();for(var _93=0,_94=_92.length;_93<_94;_93++){$k[$j++]=$g(_92,_93);$_.addtobin()}};$_.encC=function(){for(var _96=0;_96<1;_96++){if($_.i==$_.segstart){for(var _99=0;_99<1;_99++){if($_.i>$_.segend-7){$k[$j++]=0;break}if($g($_.msg,$_.segstart)!=91){$k[$j++]=0;break}if($g($_.msg,$_.segstart+1)!=41){$k[$j++]=0;break}if($g($_.msg,$_.segstart+2)!=62){$k[$j++]=0;break}if($g($_.msg,$_.segstart+3)!=30){$k[$j++]=0;break}var _9Q=$g($_.msg,$_.segstart+4);if(_9Q<48||_9Q>57){$k[$j++]=0;break}var _9T=$g($_.msg,$_.segstart+5);if(_9T<48||_9T>57){$k[$j++]=0;break}if($g($_.msg,$_.segend-1)!=4){$k[$j++]=0;break}var _9d=$f(($g($_.msg,$_.segstart+4)-48)*10+($g($_.msg,$_.segstart+5)-48));$k[$j++]=_9d;if(_9d!=5&&(_9d!=6&&_9d!=12)){$k[$j-1]=$_.dotcode_mac;break}if($g($_.msg,$_.segstart+6)!=29){$k[$j-1]=0;break}if($g($_.msg,$_.segend-2)!=30){$k[$j-1]=0;break}var _9k=$k[$j-1];if(_9k==5){$k[$j-1]=$_.dotcode_m05;break}var _9l=$k[--$j];if(_9l==6){$k[$j++]=$_.dotcode_m06;break}$k[$j++]=$_.dotcode_m12;break}var _9m=$k[--$j];$_.inmac=_9m;if($_.inmac!=0){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_lab)]);$_.addtocws();$_.mode=$_.B;$k[$j++]=$a([$g($_.Bvals,$_.inmac)]);$_.addtocws();if($_.inmac==$_.dotcode_mac){$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.segstart+4))]);$_.addtocws();$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.segstart+5))]);$_.addtocws();$_.i=$_.i+6}else{$_.i=$_.i+7}break}}if($_.i==$_.segstart){if($g($_.nDigits,$_.i)>=2){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_fn1)]);$_.addtocws()}if($g($_.msg,$_.i)==$_.dotcode_fn1&&$g($_.nDigits,$_.i+1)>=2){$_.i=$_.i+1}}if($g($_.SeventeenTen,$_.i)){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_aim),$f(($g($_.msg,$_.i+2)-48)*10+($g($_.msg,$_.i+3)-48)),$f(($g($_.msg,$_.i+4)-48)*10+($g($_.msg,$_.i+5)-48)),$f(($g($_.msg,$_.i+6)-48)*10+($g($_.msg,$_.i+7)-48))]);$_.addtocws();$_.i=$_.i+10;break}if($g($_.DatumC,$_.i)){var _Au=$g($_.msg,$_.i);if(_Au==$_.dotcode_fn1||(_Au==$_.dotcode_fn2||_Au==$_.dotcode_fn3)){$k[$j++]=$a([$g($_.Cvals,$g($_.msg,$_.i))]);$_.addtocws();if($g($_.ECI,$_.i)){$k[$j++]=Infinity;$_.ECIabc();var _B4=$a();$k[$j++]=_B4;$_.addtocws();$_.i=$_.i+7;break}if($g($_.msg,$_.i)==$_.dotcode_fn3&&$_.segstart!=$_.i){$_.i=$_.i+1;$_.inmac=0;$_.segstart=$_.i;$_.segend=$f($_.i+$g($_.UntilEndSeg,$_.i));break}$_.i=$_.i+1;break}$k[$j++]=$a([$f(($g($_.msg,$_.i)-48)*10+($g($_.msg,$_.i+1)-48))]);$_.addtocws();$_.i=$_.i+2;break}if($g($_.Binary,$_.i)){if($g($_.nDigits,$_.i+1)>0){if($g($_.msg,$_.i)<160){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_bsa),$g($_.Avals,$g($_.msg,$_.i)-128)]);$_.addtocws()}else{$k[$j++]=$a([$g($_.Cvals,$_.dotcode_bsb),$g($_.Bvals,$g($_.msg,$_.i)-128)]);$_.addtocws()}$_.i=$_.i+1;break}$k[$j++]=$a([$g($_.Cvals,$_.dotcode_bin)]);$_.addtocws();$_.mode=$_.BIN;break}$_.m=$g($_.AheadA,$_.i);$_.n=$g($_.AheadB,$_.i);if($gt($_.m,$_.n)){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_laa)]);$_.addtocws();$_.mode=$_.A;break}if($_.i==$_.segstart){var _CA=$g($_.msg,$_.i);if(_CA==9||(_CA==28||(_CA==29||_CA==30))){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_laa)]);$_.addtocws();$_.mode=$_.A;break}}if($_.n>4){$k[$j++]=$a([$g($_.Cvals,$_.dotcode_lab)]);$_.addtocws();$_.mode=$_.B;break}$k[$j++]=$a([$g($_.Cvals,$g($a([$_.dotcode_sfb,$_.dotcode_sb2,$_.dotcode_sb3,$_.dotcode_sb4]),$_.n-1))]);$_.addtocws();for(var _CR=0,_CS=$_.n;_CR<_CS;_CR++){if($g($_.msg,$_.i)==13){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_crl)]);$_.addtocws();$_.i=$_.i+2}else{$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1}}break}};$_.encB=function(){for(var _Ch=0;_Ch<1;_Ch++){$_.n=$g($_.TryC,$_.i);if($_.n>=2){if($_.n>4){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_lac)]);$_.addtocws();$_.mode=$_.C;break}$k[$j++]=$a([$g($_.Bvals,$g($a([$_.dotcode_sfc,$_.dotcode_sc2,$_.dotcode_sc3,$_.dotcode_sc4]),$_.n-1))]);$_.addtocws();for(var _Cy=0,_Cz=$_.n;_Cy<_Cz;_Cy++){if($g($_.msg,$_.i)<0){$k[$j++]=$a([$g($_.Cvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1}else{$k[$j++]=$a([$f(($g($_.msg,$_.i)-48)*10+($g($_.msg,$_.i+1)-48))]);$_.addtocws();$_.i=$_.i+2}}break}if($g($_.DatumB,$_.i)){var _DN=$g($_.msg,$_.i);if(_DN==$_.dotcode_fn1||(_DN==$_.dotcode_fn2||_DN==$_.dotcode_fn3)){$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.i))]);$_.addtocws();if($g($_.ECI,$_.i)){$k[$j++]=Infinity;$_.ECIabc();var _DX=$a();$k[$j++]=_DX;$_.addtocws();$_.i=$_.i+7;break}if($g($_.msg,$_.i)==$_.dotcode_fn3&&$_.i!=$_.segstart){$_.i=$_.i+1;$_.mode=$_.C;$_.inmac=0;$_.segstart=$_.i;$_.segend=$f($_.i+$g($_.UntilEndSeg,$_.i));break}$_.i=$_.i+1;break}if($g($_.msg,$_.i)==13&&$_.i<$_.msglen-1){if($g($_.msg,$_.i+1)==10){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_crl)]);$_.addtocws();$_.i=$_.i+2;break}}$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1;break}if($g($_.Binary,$_.i)){if($g($_.DatumB,$_.i+1)){if($g($_.msg,$_.i)<160){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_bsa),$g($_.Avals,$g($_.msg,$_.i)-128)]);$_.addtocws()}else{$k[$j++]=$a([$g($_.Bvals,$_.dotcode_bsb),$g($_.Bvals,$g($_.msg,$_.i)-128)]);$_.addtocws()}$_.i=$_.i+1;break}$k[$j++]=$a([$g($_.Bvals,$_.dotcode_bin)]);$_.addtocws();$_.mode=$_.BIN;break}if($g($_.AheadA,$_.i)==1){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_sfa),$g($_.Avals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1;break}$k[$j++]=$a([$g($_.Bvals,$_.dotcode_laa)]);$_.addtocws();$_.mode=$_.A;break}};$_.encA=function(){for(var _Ep=0;_Ep<1;_Ep++){$_.n=$g($_.TryC,$_.i);if($_.n>=2){if($_.n>4){$k[$j++]=$a([$g($_.Avals,$_.dotcode_lac)]);$_.addtocws();$_.mode=$_.C;break}$k[$j++]=$a([$g($_.Avals,$g($a([$_.dotcode_sfc,$_.dotcode_sc2,$_.dotcode_sc3,$_.dotcode_sc4]),$_.n-1))]);$_.addtocws();for(var _F6=0,_F7=$_.n;_F6<_F7;_F6++){if($g($_.msg,$_.i)<0){$k[$j++]=$a([$g($_.Cvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1}else{$k[$j++]=$a([$f(($g($_.msg,$_.i)-48)*10+($g($_.msg,$_.i+1)-48))]);$_.addtocws();$_.i=$_.i+2}}break}if($g($_.DatumA,$_.i)){var _FV=$g($_.msg,$_.i);if(_FV==$_.dotcode_fn1||(_FV==$_.dotcode_fn2||_FV==$_.dotcode_fn3)){$k[$j++]=$a([$g($_.Avals,$g($_.msg,$_.i))]);$_.addtocws();if($g($_.ECI,$_.i)){$k[$j++]=Infinity;$_.ECIabc();var _Ff=$a();$k[$j++]=_Ff;$_.addtocws();$_.i=$_.i+7;break}if($g($_.msg,$_.i)==$_.dotcode_fn3&&$_.i!=$_.segstart){$_.i=$_.i+1;$_.mode=$_.C;$_.inmac=0;$_.segstart=$_.i;$_.segend=$f($_.i+$g($_.UntilEndSeg,$_.i));break}$_.i=$_.i+1;break}$k[$j++]=$a([$g($_.Avals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1;break}if($g($_.Binary,$_.i)){if($g($_.DatumA,$_.i+1)){if($g($_.msg,$_.i)<160){$k[$j++]=$a([$g($_.Avals,$_.dotcode_bsa),$g($_.Avals,$g($_.msg,$_.i)-128)]);$_.addtocws()}else{$k[$j++]=$a([$g($_.Avals,$_.dotcode_bsb),$g($_.Bvals,$g($_.msg,$_.i)-128)]);$_.addtocws()}$_.i=$_.i+1;break}$k[$j++]=$a([$g($_.Avals,$_.dotcode_bin)]);$_.addtocws();$_.mode=$_.BIN;break}$_.n=$g($_.AheadB,$_.i);if($_.n>6){$k[$j++]=$a([$g($_.Avals,$_.dotcode_lab)]);$_.addtocws();$_.mode=$_.B;break}$k[$j++]=$a([$g($_.Avals,$g($a([$_.dotcode_sfb,$_.dotcode_sb2,$_.dotcode_sb3,$_.dotcode_sb4,$_.dotcode_sb5,$_.dotcode_sb6]),$_.n-1))]);$_.addtocws();for(var _Gk=0,_Gl=$_.n;_Gk<_Gl;_Gk++){if($g($_.msg,$_.i)==13){$k[$j++]=$a([$g($_.Bvals,$_.dotcode_crl)]);$_.addtocws();$_.i=$_.i+2}else{$k[$j++]=$a([$g($_.Bvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1}}break}};$_.encBIN=function(){for(var _H0=0;_H0<1;_H0++){$_.n=$g($_.TryC,$_.i);if($_.n>=2){$k[$j++]=Infinity;$_.finaliseBIN();var _H5=$a();$k[$j++]=_H5;$_.addtocws();if($_.n>7){$k[$j++]=$a([$g($_.BINvals,$_.dotcode_tmc)]);$_.addtocws();$_.mode=$_.C;break}$k[$j++]=$a([$g($_.BINvals,$g($a([$_.dotcode_sc2,$_.dotcode_sc3,$_.dotcode_sc4,$_.dotcode_sc5,$_.dotcode_sc6,$_.dotcode_sc7]),$_.n-2))]);$_.addtocws();for(var _HI=0,_HJ=$_.n;_HI<_HJ;_HI++){if($g($_.msg,$_.i)<0){$k[$j++]=$a([$g($_.Cvals,$g($_.msg,$_.i))]);$_.addtocws();$_.i=$_.i+1}else{$k[$j++]=$a([$f(($g($_.msg,$_.i)-48)*10+($g($_.msg,$_.i+1)-48))]);$_.addtocws();$_.i=$_.i+2}}break}if($g($_.ECI,$_.i)&&$g($_.Binary,$_.i+7)){$k[$j++]=Infinity;$_.ECIbin();var _Hi=$a();$k[$j++]=_Hi;$_.addtocws();$_.i=$_.i+7;if($_.i==$_.msglen){$k[$j++]=Infinity;$_.finaliseBIN();var _Hm=$a();$k[$j++]=_Hm;$_.addtocws()}break}if($g($_.msg,$_.i)>=0){if($g($_.Binary,$_.i)||$g($_.Binary,$_.i+1)||$g($_.Binary,$_.i+2)||$g($_.Binary,$_.i+3)||$g($_.ECI,$_.i+1)&&$g($_.Binary,$_.i+8)){$k[$j++]=Infinity;$k[$j++]=$g($_.msg,$_.i);$_.addtobin();var _IB=$a();$k[$j++]=_IB;$_.addtocws();$_.i=$_.i+1;if($_.i==$_.msglen){$k[$j++]=Infinity;$_.finaliseBIN();var _IF=$a();$k[$j++]=_IF;$_.addtocws()}break}}$k[$j++]=Infinity;$_.finaliseBIN();var _IG=$a();$k[$j++]=_IG;$_.addtocws();if($g($_.msg,$_.i)==$_.dotcode_fn3&&$_.i!=$_.segstart){$k[$j++]=$a([$g($_.BINvals,$_.dotcode_tms)]);$_.addtocws();$_.i=$_.i+1;$_.mode=$_.C;$_.inmac=0;$_.segstart=$_.i;$_.segend=$f($_.i+$g($_.UntilEndSeg,$_.i));break}if($gt($g($_.AheadA,$_.i),$g($_.AheadB,$_.i))){$k[$j++]=$a([$g($_.BINvals,$_.dotcode_tma)]);$_.addtocws();$_.mode=$_.A}else{$k[$j++]=$a([$g($_.BINvals,$_.dotcode_tmb)]);$_.addtocws();$_.mode=$_.B}break}};$_.cws=$a($_.msglen*2+1);$_.mode=$_.C;$_.bvals=$a(5);$_.bpos=0;$_.inmac=0;$_.segstart=0;$_.segend=$g($_.UntilEndSeg,0);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.msglen){break}if($_.inmac!=0){if($_.inmac!=$_.dotcode_mac&&$_.i==$_.segend-2){$_.i=$_.i+2;if($_.i>=$_.msglen){break}}if($_.inmac==$_.dotcode_mac&&$_.i==$_.segend-1){$_.i=$_.i+1;if($_.i>=$_.msglen){break}}}var _J7=$_[$g($_.dotcode_encfuncs,$_.mode)];if(_J7()===true){break}}$_.cws=$G($_.cws,0,$_.j);$_.nd=$_.cws.length;$_.minarea=(($_.nd+3+~~($_.nd/2))*9+2)*2;if($_.ratio!=-1){$_.hgt=$sqrt($_.minarea/$_.ratio);$_.wid=$sqrt($_.minarea*$_.ratio);$_.h=~~$_.hgt;$_.w=~~$_.wid;if(($_.h+$_.w)%2==1){if($_.h*$_.w<$_.minarea){$_.h=$_.h+1;$_.w=$_.w+1}}else{if($_.hgt*$_.w<$_.wid*$_.h){$_.w=$_.w+1;if($_.h*$_.w<$_.minarea){$_.w=$_.w-1;$_.h=$_.h+1;if($_.h*$_.w<$_.minarea){$_.w=$_.w+2}}}else{$_.h=$_.h+1;if($_.h*$_.w<$_.minarea){$_.h=$_.h-1;$_.w=$_.w+1;if($_.h*$_.w<$_.minarea){$_.h=$_.h+2}}}}$_.rows=$_.h;$_.columns=$_.w}else{if($_.columns==-1){var _Jw=~~(($_.minarea+$_.rows-1)/$_.rows);if((_Jw+$_.rows)%2==0){_Jw=_Jw+1}$_.columns=_Jw;if($_.columns<5){var _K5=5;if($_.rows!=-1){_K5=$f(_K5+$_.rows%2)}$_.columns=_K5}}if($_.rows==-1){var _KB=~~(($_.minarea+$_.columns-1)/$_.columns);if((_KB+$_.columns)%2==0){_KB=_KB+1}$_.rows=_KB;if($_.rows<5){$_.rows=5+$_.columns%2}}}$_.ndots=~~($_.rows*$_.columns/2);for(;;){var _KL=$_.nd+1;if((_KL+(~~(_KL/2)+3))*9+2>$_.ndots){break}$_.nd=$_.nd+1}$_.nc=~~($_.nd/2)+3;$_.nw=$_.nd+$_.nc;$_.rembits=$_.ndots-($_.nw*9+2);if($_.nd>$_.cws.length){$k[$j++]=Infinity;$q($_.cws);$k[$j++]=$_.mode==$_.BIN?109:106;for(var _Kb=0,_Kc=$_.nd-$_.cws.length-1;_Kb<_Kc;_Kb++){$k[$j++]=106}$_.cws=$a()}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#33962";$k[$j++]=$_.cws;bwipp_raiseerror()}if($_.nw*9>$_.ndots-2){$k[$j++]="bwipp.dotcodeTooLong#33968";$k[$j++]="Maximum length exceeded";bwipp_raiseerror()}if($_.rows%2==0){$_.sixedges=$a([$a([$_.columns-1,$_.rows-2]),$a([0,$_.rows-2]),$a([$_.columns-2,$_.rows-1]),$a([1,$_.rows-1]),$a([$_.columns-1,0]),$a([0,0])])}else{$_.sixedges=$a([$a([$_.columns-2,0]),$a([$_.columns-2,$_.rows-1]),$a([$_.columns-1,1]),$a([$_.columns-1,$_.rows-2]),$a([0,0]),$a([0,$_.rows-1])])}$_.dmv=function(){var _LA=$k[--$j];var _LB=$k[--$j];$k[$j++]=$f(_LB+_LA*$_.columns)};$k[$j++]=$_.rows*$_.columns;try{var _LE=$k[--$j];$k[$j++]=$a(_LE);var _LG=false}catch(e){$k[$j++]=true}if(_LG){$k[$j-1]="bwipp.dotcodeSymbolTooLarge#34002";$k[$j++]="The resulting symbol exceeds the implementation maximum array size";bwipp_raiseerror()}var _LH=$k[--$j];$_.outline=_LH;for(var _LK=0,_LJ=$_.rows-1;_LK<=_LJ;_LK+=1){$_.y=_LK;for(var _LN=0,_LM=$_.columns-1;_LN<=_LM;_LN+=1){$_.x=_LN;$k[$j++]=$_.outline;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.dmv();var _LT=$k[--$j];var _LU=$k[--$j];$p(_LU,_LT,($_.x+$_.y)%2-1)}}var _LV=$_.sixedges;for(var _LW=0,_LX=_LV.length;_LW<_LX;_LW++){$k[$j++]=$_.outline;$q($g(_LV,_LW));$_.dmv();var _La=$k[--$j];var _Lb=$k[--$j];$p(_Lb,_La,1)}$_.clearcol=function(){var _Lc=$k[--$j];$_.x=_Lc;$k[$j++]=true;for(var _Lg=$_.x&1,_Lf=$_.rows-1;_Lg<=_Lf;_Lg+=2){$k[$j++]=$_.x;$k[$j++]=_Lg;$_.dmv();var _Lj=$k[--$j];if($g($_.sym,_Lj)==1){$k[$j-1]=false;break}}};$_.clearrow=function(){var _Ll=$k[--$j];$_.y=_Ll;$k[$j++]=true;for(var _Lp=$_.y&1,_Lo=$_.columns-1;_Lp<=_Lo;_Lp+=2){$k[$j++]=_Lp;$k[$j++]=$_.y;$_.dmv();var _Ls=$k[--$j];if($g($_.sym,_Ls)==1){$k[$j-1]=false;break}}};$_.evalsymbol=function(){var _Lu=$k[--$j];$_.sym=_Lu;$_.worst=9999999;var _Lz=$a([$a(["x",0]),$a(["x",1]),$a(["y",0]),$a(["y",1])]);for(var _M0=0,_M1=_Lz.length;_M0<_M1;_M0++){$q($g(_Lz,_M0));var _M3=$k[--$j];$_.fl=_M3;var _M4=$k[--$j];$_.dir=_M4;$_.sum=0;$_.first=-1;$_.last=-1;var _M6=$eq($_.dir,"x")?$_.columns:$_.rows;for(var _M8=0,_M7=_M6-1;_M8<=_M7;_M8+=1){$_[$_.dir]=_M8;var _MA=$_.sym;var _MC=$_[$_.dir];var _ME=$eq($_.dir,"x")?$_.rows:$_.columns;var _MH=(_ME-1)*$_.fl;if($eq($_.dir,"y")){var _=_MH;_MH=_MC;_MC=_}$k[$j++]=_MA;$k[$j++]=_MC;$k[$j++]=_MH;$_.dmv();var _MI=$k[--$j];var _MJ=$k[--$j];if($g(_MJ,_MI)==1){if($_.first==-1){var _MN=$_[$_.dir];$_.first=_MN}var _MP=$_[$_.dir];$_.last=_MP;$_.sum=$_.sum+1}}var _MV=$eq($_.dir,"x")?$_.rows:$_.columns;var _MW=$f($_.sum+$_.last-$_.first)*_MV;$k[$j++]=_MW;if(_MW<$_.worst){var _MY=$k[--$j];$_.worst=_MY}else{$j--}}$_.pen=0;if($_.rows%2==1||$_.rows<=12){$_.sum=0;$_.p=0;for(var _Md=1,_Mc=$_.columns-2;_Md<=_Mc;_Md+=1){$k[$j++]=_Md;$_.clearcol();var _Me=$k[--$j];if(_Me){$_.sum=$_.sum+1;var _Mk=$_.sum==1?$_.rows:$_.p*$_.rows;$_.p=_Mk}else{$_.sum=0;$_.pen=$_.pen+$_.p;$_.p=0}}$_.pen=$_.pen+$_.p}if($_.rows%2==0||$_.columns<=12){$_.sum=0;$_.p=0;for(var _Mu=1,_Mt=$_.rows-2;_Mu<=_Mt;_Mu+=1){$k[$j++]=_Mu;$_.clearrow();var _Mv=$k[--$j];if(_Mv){$_.sum=$_.sum+1;var _N1=$_.sum==1?$_.columns:$_.p*$_.columns;$_.p=_N1}else{$_.sum=0;$_.pen=$_.pen+$_.p;$_.p=0}}$_.pen=$_.pen+$_.p}$k[$j++]=Infinity;for(var _N8=0,_N9=($_.columns+4)*2;_N8<_N9;_N8++){$k[$j++]=0}for(var _ND=0,_NE=$_.columns,_NC=$_.sym.length-1;_NE<0?_ND>=_NC:_ND<=_NC;_ND+=_NE){$k[$j++]=0;$k[$j++]=0;$q($G($_.sym,_ND,$_.columns));$k[$j++]=0;$k[$j++]=0}for(var _NJ=0,_NK=($_.columns+4)*2;_NJ<_NK;_NJ++){$k[$j++]=0}$_.symp=$a();$_.columns=$_.columns+4;$_.rows=$_.rows+4;$_.sum=0;for(var _NP=2,_NO=$_.rows-3;_NP<=_NO;_NP+=1){$_.y=_NP;for(var _NT=($_.y&1)+2,_NS=$_.columns-3;_NT<=_NS;_NT+=2){$_.x=_NT;for(var _NU=0;_NU<1;_NU++){$k[$j++]=$_.symp;$k[$j++]=$_.x-1;$k[$j++]=$_.y-1;$_.dmv();var _NY=$k[--$j];var _NZ=$k[--$j];if($g(_NZ,_NY)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x+1;$k[$j++]=$_.y-1;$_.dmv();var _Ne=$k[--$j];var _Nf=$k[--$j];if($g(_Nf,_Ne)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x-1;$k[$j++]=$_.y+1;$_.dmv();var _Nk=$k[--$j];var _Nl=$k[--$j];if($g(_Nl,_Nk)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x+1;$k[$j++]=$_.y+1;$_.dmv();var _Nq=$k[--$j];var _Nr=$k[--$j];if($g(_Nr,_Nq)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.dmv();var _Nw=$k[--$j];var _Nx=$k[--$j];if($g(_Nx,_Nw)==0){$_.sum=$_.sum+1;break}$k[$j++]=$_.symp;$k[$j++]=$_.x-2;$k[$j++]=$_.y;$_.dmv();var _O3=$k[--$j];var _O4=$k[--$j];if($g(_O4,_O3)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x;$k[$j++]=$_.y-2;$_.dmv();var _O9=$k[--$j];var _OA=$k[--$j];if($g(_OA,_O9)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x+2;$k[$j++]=$_.y;$_.dmv();var _OF=$k[--$j];var _OG=$k[--$j];if($g(_OG,_OF)==1){break}$k[$j++]=$_.symp;$k[$j++]=$_.x;$k[$j++]=$_.y+2;$_.dmv();var _OL=$k[--$j];var _OM=$k[--$j];if($g(_OM,_OL)==1){break}$_.sum=$_.sum+1}}}$_.columns=$_.columns-4;$_.rows=$_.rows-4;if($_.worst==0){$k[$j++]=-99999}else{$k[$j++]=$f($_.worst-$_.sum*$_.sum-$_.pen)}};$_.step=~~($_.nw/112)+1;$_.offset=function(){var _OY=$k[--$j];$k[$j++]=$f(_OY*$_.step+$_.start)};$_.bestscore=-99999999;$k[$j++]="masks";if($_.mask!=-1){$k[$j++]=$a([$_.mask])}else{if($_.fast){$k[$j++]=$a([3,2,1,0])}else{$k[$j++]=$a([0,1,2,3])}}var _Og=$k[--$j];var _Oh=$k[--$j];$_[_Oh]=_Og;$_.litmasks=$a(4);$F($_.masks,function(){var _Ok=$k[--$j];$_.mask=_Ok;$k[$j++]=Infinity;var _Oo=$_.cws;$k[$j++]=$_.mask;$k[$j++]=$g($_.dotcode_maskvals,$_.mask);$k[$j++]=0;for(var _Op=0,_Oq=_Oo.length;_Op<_Oq;_Op++){var _Os=$k[--$j];var _Ot=$k[--$j];$k[$j++]=$f($g(_Oo,_Op)+_Os)%113;$k[$j++]=_Ot;$k[$j++]=$f(_Os+_Ot)}$j-=2;for(var _Ov=0,_Ow=$_.nc;_Ov<_Ow;_Ov++){$k[$j++]=0}$_.rscws=$a();for(var _Oz=0,_Oy=$_.step-1;_Oz<=_Oy;_Oz+=1){$_.start=_Oz;$_.ND=~~(($_.nd+1-$_.start+$_.step-1)/$_.step);$_.NW=~~(($_.nw+1-$_.start+$_.step-1)/$_.step);$_.NC=$_.NW-$_.ND;$k[$j++]="coeffs";$k[$j++]=$_.NC;$_.dotcode_coeffscachefetch();var _PB=$k[--$j];var _PC=$k[--$j];$_[_PC]=_PB;$_.lfsr=$a($_.NC);var _PF=$_.ND;var _PG=$_.coeffs;var _PH=$_.NC;var _PI=$_.lfsr;$k[$j++]=function(){var _PK=$k[--$j];$k[$j++]=$g($_.rscws,$f(_PK*$_.step+$_.start))};$k[$j++]=_PF;$k[$j++]=_PG;$k[$j++]=_PH;$k[$j++]=_PI;$k[$j++]=113;bwipp_rsecprime();for(var _PQ=0,_PP=$_.NC-1;_PQ<=_PP;_PQ+=1){$_.j=_PQ;$k[$j++]=$_.rscws;$k[$j++]=$_.ND+$_.j;$_.offset();var _PX=$k[--$j];var _PY=$k[--$j];$p(_PY,_PX,$g($_.lfsr,$_.j))}}if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#34212";$k[$j++]=$_.rscws;bwipp_raiseerror()}$_.bits=$s($_.ndots);$P($_.bits,0,$g($_.dotcode_maskbits,$_.mask));for(var _Pj=1,_Pi=$_.nw;_Pj<=_Pi;_Pj+=1){$_.i=_Pj;$P($_.bits,($_.i-1)*9+2,$g($_.dotcode_encs,$g($_.rscws,$_.i)))}if($_.rembits>0){$P($_.bits,$_.nw*9+2,$G("11111111111111111",0,$_.rembits))}var _Pv=$_.outline;$_.pixs=$A($a(_Pv.length),_Pv);$_.posx=0;var _Q0=$_.rows%2==0?0:$_.rows-1;$_.posy=_Q0;var _Q4=$G($_.bits,0,$_.bits.length-6);for(var _Q5=0,_Q6=_Q4.length;_Q5<_Q6;_Q5++){$k[$j++]=$g(_Q4,_Q5);for(;;){$k[$j++]=$_.pixs;$k[$j++]=$_.posx;$k[$j++]=$_.posy;$_.dmv();var _QB=$k[--$j];var _QC=$k[--$j];if($g(_QC,_QB)==-1){break}if($_.rows%2==0){$_.posy=$_.posy+1;if($_.posy==$_.rows){$_.posy=0;$_.posx=$_.posx+1}}else{$_.posx=$_.posx+1;if($_.posx==$_.columns){$_.posx=0;$_.posy=$_.posy-1}}}$k[$j++]=$_.pixs;$k[$j++]=$_.posx;$k[$j++]=$_.posy;$_.dmv();var _QQ=$k[$j-3];var _QR=$k[$j-1];var _QS=$k[$j-2];$j-=3;$p(_QS,_QR,_QQ-48)}for(var _QT=0;_QT<=5;_QT+=1){$_.i=_QT;$k[$j++]=$_.pixs;$q($g($_.sixedges,$_.i));$_.dmv();var _Qb=$g($_.bits,$_.bits.length-6+$_.i);var _Qc=$k[--$j];var _Qd=$k[--$j];$p(_Qd,_Qc,_Qb-48)}$k[$j++]="score";$k[$j++]=$_.pixs;$_.evalsymbol();var _Qf=$k[--$j];var _Qg=$k[--$j];$_[_Qg]=_Qf;if($_.score>$_.bestscore){$_.bestsym=$_.pixs;$_.bestscore=$_.score;if($_.fast&&$_.bestscore>~~($_.rows*$_.columns/2)){return true}}var _Qp=$_.pixs;$_.litmask=$A($a(_Qp.length),_Qp);for(var _Qs=0;_Qs<=5;_Qs+=1){$_.i=_Qs;$k[$j++]=$_.litmask;$q($g($_.sixedges,$_.i));$_.dmv();var _Qx=$k[--$j];var _Qy=$k[--$j];$p(_Qy,_Qx,1)}$p($_.litmasks,$_.mask,$_.litmask);if($_.fast){$k[$j++]="score";$k[$j++]=$_.litmask;$_.evalsymbol();var _R4=$k[--$j];var _R5=$k[--$j];$_[_R5]=_R4;if($gt($_.score,$_.bestscore)){$_.bestsym=$_.litmask;$_.bestscore=$_.score;if($_.bestscore>~~($_.rows*$_.columns/2)){return true}}}});$_.pixs=$_.bestsym;if(!$_.fast&&$_.bestscore<=~~($_.rows*$_.columns/2)){$_.bestscore=-99999999;$F($_.masks,function(){var _RK=$k[--$j];$_.litmask=$g($_.litmasks,_RK);$k[$j++]="score";$k[$j++]=$_.litmask;$_.evalsymbol();var _RN=$k[--$j];var _RO=$k[--$j];$_[_RO]=_RN;if($_.score>$_.bestscore){$_.bestsym=$_.litmask;$_.bestscore=$_.score}});$_.pixs=$_.bestsym}$k[$j++]=new Map([["ren","renmatrix"],["dotty",true],["pixs",$_.pixs],["pixx",$_.columns],["pixy",$_.rows],["height",$_.rows*$_.modunit/72],["width",$_.columns*$_.modunit/72],["borderleft",3],["borderright",3],["bordertop",3],["borderbottom",3],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_ultracode(){if(!bwipp_ultracode.globals){var $__=$_;$_=bwipp_ultracode.globals={};$_.ultracode_metrics=$a([$a([2,7,37,5]),$a([3,36,84,13]),$a([4,85,161,22]),$a([5,142,282,29])]);$_.ultracode_tiles=$a([13135,13136,13153,13156,13163,13165,13513,13515,13516,13531,13535,13536,13561,13563,13565,13613,13615,13616,13631,13635,13636,13651,13653,13656,15135,15136,15153,15163,15165,15313,15315,15316,15351,15353,15356,15361,15363,15365,15613,15615,15616,15631,15635,15636,15651,15653,15656,16135,16136,16153,16156,16165,16313,16315,16316,16351,16353,16356,16361,16363,16365,16513,16515,16516,16531,16535,16536,16561,16563,16565,31315,31316,31351,31356,31361,31365,31513,31515,31516,31531,31535,31536,31561,31563,31565,31613,31615,31631,31635,31636,31651,31653,31656,35131,35135,35136,35151,35153,35156,35161,35163,35165,35315,35316,35351,35356,35361,35365,35613,35615,35616,35631,35635,35636,35651,35653,35656,36131,36135,36136,36151,36153,36156,36163,36165,36315,36316,36351,36356,36361,36365,36513,36515,36516,36531,36535,36536,36561,36563,36565,51313,51315,51316,51351,51353,51356,51361,51363,51365,51513,51516,51531,51536,51561,51563,51613,51615,51616,51631,51635,51636,51651,51653,51656,53131,53135,53136,53151,53153,53156,53161,53163,53165,53513,53516,53531,53536,53561,53563,53613,53615,53616,53631,53635,53636,53651,53653,53656,56131,56135,56136,56151,56153,56156,56161,56163,56165,56313,56315,56316,56351,56353,56356,56361,56363,56365,56513,56516,56531,56536,56561,56563,61313,61315,61316,61351,61353,61356,61361,61363,61365,61513,61515,61516,61531,61535,61536,61561,61563,61565,61615,61631,61635,61651,61653,63131,63135,63136,63151,63153,63156,63161,63163,63165,63513,63515,63516,63531,63535,63536,63561,63563,63565,63613,63615,63631,63635,63651,63653,65131,65135,65136,65151,65153,65156,65161,65163,65165,65313,65315,65316,65351,65353,65356,65361,65363,65365,65613,65615,65631,65635,65651,65653,56565,51515]);$_.ultracode_dccurev1=$a([51363,51563,51653,53153,53163,53513,53563,53613,53653,56153,56163,56313,56353,56363,56513,56563,51316,51356,51536,51616,53156,53516,53536,53616,53636,53656,56136,56156,56316,56356,56516,56536]);$_.ultracode_dcclrev1=$a([61351,61361,61531,61561,61631,61651,63131,63151,63161,63531,63561,63631,65131,65161,65351,65631,31351,31361,31531,31561,31631,31651,35131,35151,35161,35361,35631,35651,36131,36151,36351,36531]);$_.ultracode_dccurev2=$a([15316,16316,13516,16516,13616,15616,13136,15136,16136,13536,16536,13636,13156,16156,15356,13656,15313,16313,13513,16513,13613,15613,13153,15153,16153,16353,13653,15653,13163,15163,15363,13563]);$_.ultracode_dcclrev2=$a([36315,36515,35615,35135,36135,31535,36535,31635,35635,35165,36165,31365,35365,36365,31565,36565,61315,65315,63515,61615,65135,61535,63535,61635,63635,65635,63165,65165,61365,65365,61565,63565]);var _A=new Map([[0,"FFFFFF"],[9,"000000"],[1,"00FFFF"],[3,"FF00FF"],[5,"FFFF00"],[6,"00FF00"]]);$_.ultracode_colormap=_A;$_.ultracode_qccfact=$a([0,1,2,4,6,8]);$k[$j++]=Infinity;$k[$j++]=1;for(var _C=0;_C<282;_C++){var _D=$k[$j-1];$k[$j++]=_D*3%283}var _E=$a();$_.rsalog=_E;$_.rslog=$a(283);for(var _G=1;_G<=282;_G+=1){$p($_.rslog,$g($_.rsalog,_G),_G)}$_.rslog=$_.rslog;$_.ultracode_rsprod=function(){var _L=$k[$j-1];if(_L!=0){var _N=$k[$j-2];if(_N!=0){var _P=$k[--$j];var _R=$k[--$j];$k[$j++]=$g($_.rsalog,$f($g($_.rslog,_P)+$g($_.rslog,_R))%282)}else{$j-=2;$k[$j++]=0}}else{$j-=2;$k[$j++]=0}};$_.ultracode_gencoeffs=function(){$k[$j++]=Infinity;$k[$j++]=1;for(var _X=0,_Y=$_.k;_X<_Y;_X++){$k[$j++]=0}$_.coeffs=$a();for(var _b=1,_a=$_.k;_b<=_a;_b+=1){var _c=$_.coeffs;$p(_c,_b,$g(_c,_b-1));$k[$j++]=$g($_.rsalog,_b);for(var _g=_b-1;_g>=1;_g-=1){var _h=$_.coeffs;var _j=$k[$j-1];$k[$j++]=_h;$k[$j++]=_g;$k[$j++]=$g(_h,_g);$k[$j++]=_j;$_.ultracode_rsprod();var _l=$k[--$j];var _m=$k[--$j];var _o=$k[--$j];$p(_o,_m,$f(_l+$g($_.coeffs,_m-1))%283)}var _p=$_.coeffs;var _r=$k[$j-1];$k[$j++]=_p;$k[$j++]=0;$k[$j++]=$g(_p,0);$k[$j++]=_r;$_.ultracode_rsprod();var _s=$k[--$j];var _t=$k[--$j];var _u=$k[--$j];$p(_u,_t,_s);$j--}$_.coeffs=$G($_.coeffs,0,$_.coeffs.length-1);for(var _z=$_.coeffs.length-1;_z>=0;_z-=2){var _10=$_.coeffs;$p(_10,_z,283-$g(_10,_z))}$k[$j++]=$_.coeffs};$_.ultracode_coeffscachemax=37;$_.ultracode_coeffscachelimit=1311;$k[$j++]=$_.ultracode_coeffscachemax;$k[$j++]=$_.ultracode_coeffscachelimit;bwipp_fifocache();var _15=$k[--$j];$_.ultracode_coeffscache=_15;$_.ultracode_coeffscachefetch=function(){var _16=$k[--$j];$_.k=_16;var _18=$g($_.ultracode_coeffscache,"fetch");$k[$j++]=$_.k;$k[$j++]=function(){$_.ultracode_gencoeffs()};$k[$j++]=function(){var _19=$k[--$j];$k[$j++]=_19.length};if(_18()===true){return true}};$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_ultracode.globals);$_.dontdraw=false;$_.eclevel="EC2";$_.parse=false;$_.parsefnc=false;$_.start=257;$_.link1=0;$_.raw=false;$_.rev=2;bwipp_processoptions();var _1A=$k[--$j];$_.options=_1A;var _1B=$k[--$j];$_.barcode=_1B;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.modunit=2;if($_.barcode.length>2500){$k[$j++]="bwipp.ultracodeinputTooLong#34609";$k[$j++]="The input data is too long";bwipp_raiseerror()}var _1F=$_.rev;if(_1F!=1&&_1F!=2){$k[$j++]="bwipp.ultracodeInvalidRevision#34613";$k[$j++]="Valid revisions are 1 and 2";bwipp_raiseerror()}var _1H=$_.eclevel.length==3;$k[$j++]=_1H;if(_1H){$j--;var _1K=$eq($G($_.eclevel,0,2),"EC");$k[$j++]=_1K;if(_1K){$j--;var _1N=$g($_.eclevel,2)-48;var _1P=$_.rev==2?1:0;$k[$j++]=_1N>=_1P&&_1N<=5}}var _1Q=$k[--$j];if(!_1Q){$k[$j++]="bwipp.ultracodeInvalidErrorCorrectionLevel#34622";$k[$j++]="Valid error correction levels are EC1 to EC5, and EC0 for legacy revision 1";bwipp_raiseerror()}if($_.raw){$_.dcws=$a($_.barcode.length);$_.i=0;$_.j=0;for(;;){if($_.i>=$_.barcode.length-3){break}if($g($_.barcode,$_.i)!=94){break}var _1b=$G($_.barcode,$_.i+1,3);$k[$j++]=_1b;$k[$j++]=false;for(var _1c=0,_1d=_1b.length;_1c<_1d;_1c++){var _1e=$g(_1b,_1c);if(_1e<48||_1e>57){$k[$j-1]=true}}var _1f=$k[--$j];if(_1f){$j--;break}var _1g=$k[--$j];$_.cw=$i(_1g);if($_.cw>284){$k[$j++]="bwipp.ultracodeBadRawCodeword#34641";$k[$j++]="Raw codewords must be 0 to 284";bwipp_raiseerror()}$p($_.dcws,$_.j,$_.cw);$_.i=$_.i+4;$_.j=$_.j+1}if($_.i!=$_.barcode.length){$k[$j++]="bwipp.ultracodeBadRawFormat#34648";$k[$j++]="Raw codewords must be formatted as ^NNN";bwipp_raiseerror()}$_.dcws=$G($_.dcws,0,$_.j)}else{$_.fn1=-1;$_.fn3=-2;$_.fncvals=new Map([["parse",$_.parse],["parsefnc",$_.parsefnc],["FNC1",$_.fn1],["FNC3",$_.fn3]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _1y=$k[--$j];$_.msg=_1y;$_.msglen=$_.msg.length;$_.scr=$a([]);$_.scp=$a([]);$k[$j++]=Infinity;$q($_.scr);$q($_.scp);$F($_.msg,function(){var _23=$k[$j-1];if(_23==$_.fn1){$k[$j-1]=268}var _25=$k[$j-1];if(_25==$_.fn3){$k[$j-1]=269}});$_.dcws=$a()}if($has($_.options,"debugcws")){$k[$j++]="bwipp.debugcws#34690";$k[$j++]=$_.dcws;bwipp_raiseerror()}$_.mcc=$_.dcws.length+3;$_.eclval=$g($_.eclevel,2)-48;if($_.eclval!=0){var _2K=$g($_.ultracode_qccfact,$_.eclval);var _2J=~~($_.mcc/25);if($_.mcc%25!=0){_2J=_2J+1}$_.qcc=_2K*_2J+5}else{$_.qcc=3}$_.acc=$_.qcc-3+78*$_.link1;$_.tcc=$_.mcc+$_.qcc;for(var _2Q=0,_2R=$_.ultracode_metrics.length;_2Q<_2R;_2Q++){$_.m=$g($_.ultracode_metrics,_2Q);$_.rows=$g($_.m,0);$_.minc=$g($_.m,1);$_.maxc=$g($_.m,2);$_.mcol=$g($_.m,3);$_.okay=true;if($_.tcc<$_.minc||$_.tcc>$_.maxc){$_.okay=false}if($_.okay){break}}if(!$_.okay){$k[$j++]="bwipp.ultracodeNoValidSymbol#34720";$k[$j++]="Maximum length exceeded or invalid content";bwipp_raiseerror()}for(var _2i=$_.mcol;_2i<=61;_2i+=1){$_.columns=_2i;$k[$j++]=$_.columns;if($_.columns>=15){$k[$j-1]-=1}if($_.columns>=31){$k[$j-1]-=1}if($_.columns>=47){$k[$j-1]-=1}var _2r=$k[--$j];var _2t=$f(_2r*$_.rows-3-$_.tcc);$k[$j++]=_2t;if(_2t>=0){var _2u=$k[--$j];$_.pads=_2u;break}$j--}$_.dcc=$f($_.columns-$_.mcol);$_.n=$_.mcc;$_.k=$_.qcc;$k[$j++]="coeffs";$k[$j++]=$_.k;$_.ultracode_coeffscachefetch();var _30=$k[--$j];var _31=$k[--$j];$_[_31]=_30;$k[$j++]=Infinity;$k[$j++]=$_.start;$k[$j++]=$_.mcc;$k[$j++]=$_.acc;$q($_.dcws);$_.rsdata=$a();$_.ecws=$a($_.k);var _38=$_.n;var _39=$_.coeffs;var _3A=$_.k;var _3B=$_.ecws;$k[$j++]=function(){var _3D=$k[--$j];$k[$j++]=$g($_.rsdata,_3D)};$k[$j++]=_38;$k[$j++]=_39;$k[$j++]=_3A;$k[$j++]=_3B;$k[$j++]=283;bwipp_rsecprime();if($has($_.options,"debugecc")){$k[$j++]="bwipp.debugecc#34762";$k[$j++]=$_.ecws;bwipp_raiseerror()}var _3J=$_.rev==1?$_.ultracode_dccurev1:$_.ultracode_dccurev2;$_.dccu=_3J;var _3M=$_.rev==1?$_.ultracode_dcclrev1:$_.ultracode_dcclrev2;$_.dccl=_3M;$_.rows=$_.rows*6+1;$_.columns=$_.columns+6;$k[$j++]=Infinity;for(var _3S=0,_3T=$_.rows*$_.columns;_3S<_3T;_3S++){$k[$j++]=-1}$_.pixs=$a();$_.qmv=function(){var _3V=$k[--$j];var _3W=$k[--$j];$k[$j++]=$f(_3W+_3V*$_.columns)};for(var _3Z=0,_3Y=$_.columns-1;_3Z<=_3Y;_3Z+=1){$_.i=_3Z;for(var _3c=0,_3b=$_.rows-1;_3c<=_3b;_3c+=6){$_.j=_3c;if($_.i>=5){$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.j;$_.qmv();var _3i=$k[--$j];var _3j=$k[--$j];$p(_3j,_3i,$_.i%2*9)}}$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=0;$_.qmv();var _3m=$k[--$j];var _3n=$k[--$j];$p(_3n,_3m,9);$k[$j++]=$_.pixs;$k[$j++]=$_.i;$k[$j++]=$_.rows-1;$_.qmv();var _3r=$k[--$j];var _3s=$k[--$j];$p(_3s,_3r,9)}for(var _3v=1,_3u=$_.rows-2;_3v<=_3u;_3v+=1){$_.i=_3v;for(var _3y=3,_3x=$_.columns-1;_3y<=_3x;_3y+=16){$_.j=_3y;$k[$j++]=$_.pixs;$k[$j++]=$_.j;$k[$j++]=$_.i;$_.qmv();var _43=$k[--$j];var _44=$k[--$j];$p(_44,_43,(1-$_.i%2)*9)}$k[$j++]=$_.pixs;$k[$j++]=0;$k[$j++]=$_.i;$_.qmv();var _47=$k[--$j];var _48=$k[--$j];$p(_48,_47,9);$k[$j++]=$_.pixs;$k[$j++]=1;$k[$j++]=$_.i;$_.qmv();var _4C=$k[--$j];var _4D=$k[--$j];$p(_4D,_4C,(1-$_.i%2)*9);$k[$j++]=$_.pixs;$k[$j++]=2;$k[$j++]=$_.i;$_.qmv();var _4G=$k[--$j];var _4H=$k[--$j];$p(_4H,_4G,0);$k[$j++]=$_.pixs;$k[$j++]=3;$k[$j++]=$_.i;$_.qmv();var _4K=$k[--$j];var _4L=$k[--$j];$p(_4L,_4K,9);$k[$j++]=$_.pixs;$k[$j++]=4;$k[$j++]=$_.i;$_.qmv();var _4O=$k[--$j];var _4P=$k[--$j];$p(_4P,_4O,0);$k[$j++]=$_.pixs;$k[$j++]=$_.columns-1;$k[$j++]=$_.i;$_.qmv();var _4T=$k[--$j];var _4U=$k[--$j];$p(_4U,_4T,9)}$_.i=~~($_.rows/2)-5;$k[$j++]=Infinity;var _4a=$R($s(5),$g($_.dccu,$_.dcc),10);for(var _4b=0,_4c=_4a.length;_4b<_4c;_4b++){$k[$j++]=$g(_4a,_4b)-48}var _4i=$R($s(5),$g($_.dccl,$_.dcc),10);$k[$j++]=0;for(var _4j=0,_4k=_4i.length;_4j<_4k;_4j++){$k[$j++]=$g(_4i,_4j)-48}var _4m=$a();for(var _4n=0,_4o=_4m.length;_4n<_4o;_4n++){$k[$j++]=$g(_4m,_4n);$k[$j++]=$_.pixs;$k[$j++]=2;$k[$j++]=$_.i;$_.qmv();var _4s=$k[$j-3];var _4t=$k[$j-1];var _4u=$k[$j-2];$j-=3;$p(_4u,_4t,_4s);$_.i=$_.i+1}$k[$j++]=Infinity;$k[$j++]=$_.start;$k[$j++]=$_.mcc;$q($_.ecws);$k[$j++]=$_.tcc;$k[$j++]=283;$k[$j++]=$_.acc;$q($_.dcws);for(var _53=0,_54=$_.pads;_53<_54;_53++){$k[$j++]=284}$k[$j++]=$_.qcc;$_.tileseq=$a();$_.x=5;$_.y=1;var _56=$_.tileseq;for(var _57=0,_58=_56.length;_57<_58;_57++){var _5C=$R($s(5),$g($_.ultracode_tiles,$g(_56,_57)),10);for(var _5D=0,_5E=_5C.length;_5D<_5E;_5D++){$k[$j++]=$g(_5C,_5D);$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.qmv();var _5J=$k[$j-3];var _5K=$k[$j-1];var _5L=$k[$j-2];$j-=3;$p(_5L,_5K,_5J-48);$_.y=$_.y+1}if($_.y!=$_.rows-1){$_.y=$_.y+1}else{$_.x=$_.x+1;$_.y=1;$k[$j++]=$_.pixs;$k[$j++]=$_.x;$k[$j++]=$_.y;$_.qmv();var _5U=$k[--$j];var _5V=$k[--$j];if($g(_5V,_5U)!=-1){$_.x=$_.x+1}}}$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.columns],["pixy",$_.rows],["height",$_.rows*$_.modunit/72],["width",$_.columns*$_.modunit/72],["colormap",$_.ultracode_colormap],["borderleft",1],["borderright",1],["bordertop",1],["borderbottom",1],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1_cc(){if(!bwipp_gs1_cc.globals){var $__=$_;$_=bwipp_gs1_cc.globals={};var _0=new Map([["ean13",4],["upca",4],["ean8",3],["upce",2],["gs1-128",4],["databaromni",4],["databarstacked",2],["databarstackedomni",2],["databartruncated",4],["databarlimited",3],["databarexpanded",4],["databarexpandedstacked",4]]);$_.gs1_cc_lintypecccolumns=_0;var _9=new Map([["a",$a([$a([167,138,118,108,88,78,59]),$a([167,138,118,98,78]),$a([197,167,138,108,78])])],["b",$a([$a([336,296,256,208,160,104,56]),$a([768,648,536,416,304,208,152,112,72,32]),$a([1184,1016,840,672,496,352,264,208,152,96,56])])]]);$_.gs1_cc_bitcapsmaps=_9;$_.gs1_cc_fillpat=$a([0,0,1,0,0]);$_.gs1_cc_pwr928init=$a([0,0,0,0,0,0,1]);$_.gs1_cc_tobin=function(){var _C=$k[--$j];var _D=$s(_C);$k[$j++]=_D;for(var _F=0,_E=_D.length-1;_F<=_E;_F+=1){var _G=$k[$j-1];$p(_G,_F,48)}var _H=$k[$j-1];$k[$j++]=_H;var _I=$k[$j-3];var _J=$k[$j-1];$k[$j-3]=$k[$j-2];$j-=2;var _L=$R($s(_J.length),_I,2);$P(_J,_J.length-_L.length,_L)};$_.gs1_cc_fnc1=-1;$_.gs1_cc_lnumeric=-2;$_.gs1_cc_lalphanumeric=-3;$_.gs1_cc_liso646=-4;$k[$j++]=Infinity;for(var _M=65;_M<=90;_M+=1){$k[$j++]=_M;$k[$j++]=_M-65;$k[$j++]=5;$_.gs1_cc_tobin()}for(var _N=48;_N<=57;_N+=1){$k[$j++]=_N;$k[$j++]=_N+4;$k[$j++]=6;$_.gs1_cc_tobin()}$k[$j++]=$_.gs1_cc_fnc1;$k[$j++]="11111";var _O=$d();$_.alpha=_O;$k[$j++]=Infinity;for(var _P=0;_P<=119;_P+=1){var _R=$Z($s(2),"00");var _T=$R($s(2),_P,11);$P(_R,2-_T.length,_T);$k[$j++]=_P;$k[$j++]=_R;if($g(_R,0)==65){var _V=$k[$j-1];$p(_V,0,94)}var _W=$k[$j-1];if($g(_W,1)==65){var _Y=$k[$j-1];$p(_Y,1,94)}var _Z=$k[--$j];var _a=$k[--$j];var _c=$Z($s(7),"0000000");var _e=$R($s(7),_a+8,2);$P(_c,7-_e.length,_e);$k[$j++]=_Z;$k[$j++]=_c}$k[$j++]=$_.gs1_cc_lalphanumeric;$k[$j++]="0000";var _f=$d();$_.numeric=_f;$k[$j++]=Infinity;for(var _g=48;_g<=57;_g+=1){$k[$j++]=_g;$k[$j++]=_g-43;$k[$j++]=5;$_.gs1_cc_tobin()}$k[$j++]=$_.gs1_cc_fnc1;$k[$j++]="01111";for(var _h=65;_h<=90;_h+=1){$k[$j++]=_h;$k[$j++]=_h-33;$k[$j++]=6;$_.gs1_cc_tobin()}$k[$j++]=42;$k[$j++]="111010";for(var _i=44;_i<=47;_i+=1){$k[$j++]=_i;$k[$j++]=_i+15;$k[$j++]=6;$_.gs1_cc_tobin()}$k[$j++]=$_.gs1_cc_lnumeric;$k[$j++]="000";$k[$j++]=$_.gs1_cc_liso646;$k[$j++]="00100";var _j=$d();$_.alphanumeric=_j;$k[$j++]=Infinity;for(var _k=48;_k<=57;_k+=1){$k[$j++]=_k;$k[$j++]=_k-43;$k[$j++]=5;$_.gs1_cc_tobin()}$k[$j++]=$_.gs1_cc_fnc1;$k[$j++]="01111";for(var _l=65;_l<=90;_l+=1){$k[$j++]=_l;$k[$j++]=_l-1;$k[$j++]=7;$_.gs1_cc_tobin()}for(var _m=97;_m<=122;_m+=1){$k[$j++]=_m;$k[$j++]=_m-7;$k[$j++]=7;$_.gs1_cc_tobin()}$k[$j++]=33;$k[$j++]="11101000";$k[$j++]=34;$k[$j++]="11101001";for(var _n=37;_n<=47;_n+=1){$k[$j++]=_n;$k[$j++]=_n+197;$k[$j++]=8;$_.gs1_cc_tobin()}for(var _o=58;_o<=63;_o+=1){$k[$j++]=_o;$k[$j++]=_o+187;$k[$j++]=8;$_.gs1_cc_tobin()}$k[$j++]=95;$k[$j++]="11111011";$k[$j++]=32;$k[$j++]="11111100";$k[$j++]=$_.gs1_cc_lnumeric;$k[$j++]="000";$k[$j++]=$_.gs1_cc_lalphanumeric;$k[$j++]="00100";var _p=$d();$_.iso646=_p;$k[$j++]=Infinity;$k[$j++]=$_.gs1_cc_pwr928init;for(var _q=0;_q<68;_q++){$k[$j++]=$a(7)}$_.pwr928=$a();for(var _s=1;_s<=68;_s+=1){$_.j=_s;$_.v=0;for(var _t=6;_t>=1;_t-=1){$_.i=_t;$_.v=$f($g($g($_.pwr928,$_.j-1),$_.i)*2+~~($_.v/928));$p($g($_.pwr928,$_.j),$_.i,$_.v%928)}$p($g($_.pwr928,$_.j),0,$f($g($g($_.pwr928,$_.j-1),0)*2+~~($_.v/928)))}$_.pwr928=$_.pwr928;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_gs1_cc.globals);$_.parse=false;$_.dontlint=false;$_.lintreqs=false;$_.dontdraw=false;$_.ccversion="a";$_.cccolumns=-1;$_.lintype="";$_.linwidth=-1;bwipp_processoptions();var _1E=$k[--$j];$_.options=_1E;var _1F=$k[--$j];$_.barcode=_1F;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($ne($_.ccversion,"a")&&$ne($_.ccversion,"b")&&$ne($_.ccversion,"c")){$k[$j++]="bwipp.gs1ccBadCCversion#36503";$k[$j++]="ccversion must be a, b or c";bwipp_raiseerror()}if($_.cccolumns>30){$k[$j++]="bwipp.gs1ccColumnsTooBig#36507";$k[$j++]="The maximum number of composite component columns is 30";bwipp_raiseerror()}if($_.lintype.length!=0){if(!$has($_.gs1_cc_lintypecccolumns,$_.lintype)){$k[$j++]="bwipp.gs1ccBadLinType#36512";$k[$j++]="The lintype is not recognised";bwipp_raiseerror()}}if($_.cccolumns==-1){if($eq($_.lintype,"gs1-128")&&$eq($_.ccversion,"c")){if($_.linwidth<0){$k[$j++]="bwipp.gs1ccMissingLinWidth#36522";$k[$j++]="linwidth must be set for a GS1-128 with a CC-C composite component";bwipp_raiseerror()}if($_.linwidth<68){$k[$j++]="bwipp.gs1ccMinimumLinWidth#36525";$k[$j++]="Minimum linwidth for a GS1-128 with a CC-C composite component is 68";bwipp_raiseerror()}if($_.linwidth==68){$k[$j++]=1}else{$k[$j++]=~~(($_.linwidth-52)/17)}}else{if($_.lintype.length!=0){$k[$j++]=$g($_.gs1_cc_lintypecccolumns,$_.lintype)}else{$k[$j++]=2}}var _1Z=$k[--$j];$_.cccolumns=_1Z}if($ne($_.ccversion,"c")){if($_.cccolumns<2||$_.cccolumns>4){$k[$j++]="bwipp.gs1ccBadColumns#36536";$k[$j++]="CC-A and CC-B require 2 to 4 columns";bwipp_raiseerror()}}if($_.lintype.length!=0&&$ne($_.ccversion,"c")){if($lt($_.cccolumns,$g($_.gs1_cc_lintypecccolumns,$_.lintype))){$k[$j++]="bwipp.gs1ccColumnsTooSmall#36542";$k[$j++]="The cccolumns value is too small for the linear symbology";bwipp_raiseerror()}}$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _1j=$k[--$j];$_.fncs=_1j;var _1k=$k[--$j];$_.vals=_1k;var _1l=$k[--$j];$_.ais=_1l;$_.isupper=function(){var _1m=$k[--$j];$k[$j++]=_1m>=65&&_1m<=90};$_.isnum0=function(){var _1n=$k[--$j];$k[$j++]=_1n>=48&&_1n<=57};$_.isnum1=function(){var _1o=$k[--$j];$k[$j++]=_1o>=49&&_1o<=57};for(;;){if($_.ais.length>=1){if($eq($g($_.ais,0),"10")||$eq($g($_.ais,0),"11")||$eq($g($_.ais,0),"17")){$k[$j++]=-1;$k[$j++]="10";break}}if($_.ais.length>=1){if($eq($g($_.ais,0),"90")){$_.v=$g($_.vals,0);if($_.v.length>=1){$k[$j++]=$g($_.v,0);$_.isupper();var _24=$k[--$j];if(_24){$k[$j++]=0;$k[$j++]="11";break}}if($_.v.length>=2){$k[$j++]=$g($_.v,0);$_.isnum1();$k[$j++]=$g($_.v,1);$_.isupper();var _2A=$k[--$j];var _2B=$k[--$j];if(_2B&&_2A){$k[$j++]=1;$k[$j++]="11";break}}if($_.v.length>=3){$k[$j++]=$g($_.v,0);$_.isnum1();$k[$j++]=$g($_.v,1);$_.isnum0();var _2H=$k[--$j];var _2I=$k[--$j];$k[$j++]=$an(_2I,_2H);$k[$j++]=$g($_.v,2);$_.isupper();var _2L=$k[--$j];var _2M=$k[--$j];if(_2M&&_2L){$k[$j++]=2;$k[$j++]="11";break}}if($_.v.length>=4){$k[$j++]=$g($_.v,0);$_.isnum1();$k[$j++]=$g($_.v,1);$_.isnum0();var _2S=$k[--$j];var _2T=$k[--$j];$k[$j++]=$an(_2T,_2S);$k[$j++]=$g($_.v,2);$_.isnum0();var _2W=$k[--$j];var _2X=$k[--$j];$k[$j++]=$an(_2X,_2W);$k[$j++]=$g($_.v,3);$_.isupper();var _2a=$k[--$j];var _2b=$k[--$j];if(_2b&&_2a){$k[$j++]=3;$k[$j++]="11";break}}}}$k[$j++]=-1;$k[$j++]="0";break}var _2c=$k[--$j];$_.method=_2c;var _2d=$k[--$j];$_.npre=_2d;if($eq($_.method,"10")){var _2k=$g($_.vals,0);$k[$j++]=$eq($g($_.ais,0),"11")||$eq($g($_.ais,0),"17");$k[$j++]=_2k;if(_2k.length>=6){var _2l=$k[--$j];var _2m=$G(_2l,0,6);$k[$j++]=true;for(var _2n=0,_2o=_2m.length;_2n<_2o;_2n++){var _2p=$g(_2m,_2n);if(_2p<48||_2p>57){$k[$j-1]=false}}}else{$k[$j-1]=false}var _2q=$k[--$j];var _2r=$k[--$j];if(_2r&&_2q){var _2t=$g($_.vals,0);var _2y=$Z($s(16),"0000000000000000");var _30=$R($s(16),$i($G(_2t,0,2))*384+(($i($G(_2t,2,2))-1)*32+$i($G(_2t,4,2))),2);$P(_2y,16-_30.length,_30);$k[$j++]=_2y;$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=0;$r(4,-1);var _31=$k[--$j];$F(_31,function(){$k[$j-1]-=48});$k[$j++]=$eq($g($_.ais,0),"11")?0:1;var _36=$a();$_.cdf=_36;$_.ais=$G($_.ais,1,$_.ais.length-1);$_.vals=$G($_.vals,1,$_.vals.length-1);$_.fncs=$G($_.fncs,1,$_.fncs.length-1)}else{$_.cdf=$a([1,0,1,1])}if($_.ais.length!=0){if($eq($g($_.ais,0),"10")){$k[$j++]=Infinity;$F($g($_.vals,0));$_.gpf=$a();if($_.ais.length>1){$k[$j++]=Infinity;$q($_.gpf);$k[$j++]=$_.gs1_cc_fnc1;$_.gpf=$a()}$_.ais=$G($_.ais,1,$_.ais.length-1);$_.vals=$G($_.vals,1,$_.vals.length-1);$_.fncs=$G($_.fncs,1,$_.fncs.length-1)}else{$_.gpf=$a([$_.gs1_cc_fnc1])}}else{$k[$j++]=Infinity;$q($_.cdf);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$_.cdf=$a();$_.gpf=$a([])}$_.mode="numeric"}if($eq($_.method,"11")){$_.cdf=$a([1,1]);$_.ai90=$g($_.vals,0);var _3h=$_.npre+1!=$_.ai90.length?$G($_.ai90,$_.npre+1,$f($_.ai90.length-$_.npre)-1):"";$_.ai90rem=_3h;$k[$j++]="nalpha";$k[$j++]=0;$F($_.ai90rem,function(){var _3j=$k[--$j];if(_3j>=65&&_3j<=90){$k[$j-1]+=1}});var _3l=$k[--$j];var _3m=$k[--$j];$_[_3m]=_3l;$k[$j++]="nnums";$k[$j++]=0;$F($_.ai90rem,function(){var _3o=$k[--$j];if(_3o>=48&&_3o<=57){$k[$j-1]+=1}});var _3q=$k[--$j];var _3r=$k[--$j];$_[_3r]=_3q;$k[$j++]="mode";if($gt($_.nalpha,$_.nnums)){$k[$j++]="alpha"}else{if($_.nalpha==0){$k[$j++]="numeric"}else{$k[$j++]="alphanumeric"}}var _3v=$k[--$j];var _3w=$k[--$j];$_[_3w]=_3v;if($f($_.nalpha+$_.nnums)!=$_.ai90rem.length){$_.mode="alphanumeric"}$k[$j++]=Infinity;$q($_.cdf);if($eq($_.mode,"alphanumeric")){$k[$j++]=0}if($eq($_.mode,"numeric")){$k[$j++]=1;$k[$j++]=0}if($eq($_.mode,"alpha")){$k[$j++]=1;$k[$j++]=1}$_.cdf=$a();var _47=$_.ais.length>1?$g($_.ais,1):-1;$_.ais1=_47;var _4C=$_.vals.length>1?$g($_.vals,1):-1;$_.vals1=_4C;if($eq($_.ais1,"21")||$eq($_.ais1,"8004")){$k[$j++]=Infinity;$q($_.cdf);if($eq($_.ais1,"21")){$k[$j++]=1;$k[$j++]=0}else{$k[$j++]=1;$k[$j++]=1}$_.cdf=$a()}else{$k[$j++]=Infinity;$q($_.cdf);$k[$j++]=0;$_.cdf=$a()}var _4N=$_.npre!=0?$i($G($_.ai90,0,$_.npre)):0;$_.nval=_4N;$X("BDHIJKLNPQRSTVXZ",$G($_.ai90,$_.npre,1));var _4S=$k[--$j];if(_4S){var _4T=$k[--$j];$j--;var _4V=_4T.length;$j--;$_.aval=_4V}else{$j--;$_.aval=-1}if($_.nval<31&&$_.aval!=-1){var _4b=$Z($s(5),"00000");var _4e=$R($s(5),$_.nval,2);$P(_4b,5-_4e.length,_4e);$k[$j++]=Infinity;$F(_4b,function(){$k[$j-1]-=48});var _4i=$a();$_.nbits=_4i;var _4k=$Z($s(4),"0000");var _4n=$R($s(4),$_.aval,2);$P(_4k,4-_4n.length,_4n);$k[$j++]=Infinity;$F(_4k,function(){$k[$j-1]-=48});var _4r=$a();$_.abits=_4r;$k[$j++]=Infinity;$q($_.cdf);$q($_.nbits);$q($_.abits);$_.cdf=$a()}else{var _4w=$Z($s(10),"0000000000");var _4z=$R($s(10),$_.nval,2);$P(_4w,10-_4z.length,_4z);$k[$j++]=Infinity;$F(_4w,function(){$k[$j-1]-=48});var _53=$a();$_.nbits=_53;var _55=$Z($s(5),"00000");var _5A=$R($s(5),$g($_.ai90,$_.npre)-65,2);$P(_55,5-_5A.length,_5A);$k[$j++]=Infinity;$F(_55,function(){$k[$j-1]-=48});var _5E=$a();$_.abits=_5E;$k[$j++]=Infinity;$q($_.cdf);$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$q($_.nbits);$q($_.abits);$_.cdf=$a()}if($ne($_.mode,"alpha")){$k[$j++]=Infinity;$F($_.ai90rem);if($_.ais.length>1){$k[$j++]=$_.gs1_cc_fnc1}$_.gpf=$a()}else{$k[$j++]=Infinity;$F($_.ai90rem);if($_.ais.length>1){$k[$j++]=$_.gs1_cc_fnc1}$_.in=$a();$_.out=$a($_.in.length*6);$_.j=0;for(var _5R=0,_5Q=$_.in.length-1;_5R<=_5Q;_5R+=1){var _5V=$g($_.alpha,$g($_.in,_5R));$k[$j++]=Infinity;$F(_5V,function(){$k[$j-1]-=48});var _5Z=$a();$P($_.out,$_.j,_5Z);$_.j=_5V.length+$_.j}$k[$j++]=Infinity;$q($_.cdf);$q($G($_.out,0,$_.j));$_.cdf=$a();$_.gpf=$a([]);if($_.ais.length>1){$_.mode="numeric"}}$_.ais=$G($_.ais,1,$_.ais.length-1);$_.vals=$G($_.vals,1,$_.vals.length-1);$_.fncs=$G($_.fncs,1,$_.fncs.length-1);if($_.ais.length!=0){if($eq($_.ais1,"21")||$eq($_.ais1,"8004")){$k[$j++]=Infinity;$q($_.gpf);$F($_.vals1);$_.gpf=$a();$_.ais=$G($_.ais,1,$_.ais.length-1);$_.vals=$G($_.vals,1,$_.vals.length-1);$_.fncs=$G($_.fncs,1,$_.fncs.length-1);if($_.ais.length!=0){$k[$j++]=Infinity;$q($_.gpf);$k[$j++]=$_.gs1_cc_fnc1;$_.gpf=$a()}}}}if($eq($_.method,"0")){$_.cdf=$a([0]);$_.gpf=$a([]);$_.mode="numeric"}$k[$j++]=Infinity;$q($_.gpf);for(var _6C=0,_6B=$_.ais.length-1;_6C<=_6B;_6C+=1){$_.i=_6C;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){$k[$j++]=$_.gs1_cc_fnc1}}$as($a($m()));var _6Q=$k[--$j];$_.gpf=_6Q;$j--;$_.rembits=function(){var _6R=$k[--$j];$_.used=_6R;if($ne($_.ccversion,"c")){$_.bitcaps=$g($g($_.gs1_cc_bitcapsmaps,$_.ccversion),$_.cccolumns-2);$k[$j++]=-1;$F($_.bitcaps,function(){var _6Y=$k[$j-1];if($ge(_6Y,$_.used)){var _6a=$k[--$j];var _6b=$k[--$j];$k[$j++]=_6a;$k[$j++]=_6b}$j--});var _6c=$k[$j-1];if(_6c!=-1){var _6e=$k[--$j];$k[$j++]=$f(_6e-$_.used)}}else{var _6f=new Map([["c",-1]]);var _6h=$g(_6f,$_.ccversion);var _6j=$ceil($_.used/8);$_.m=~~(_6j/6)*5+_6j%6;if($_.m<=40){$k[$j++]=8}if($_.m>=41&&$_.m<=160){$k[$j++]=16}if($_.m>=161&&$_.m<=320){$k[$j++]=32}if($_.m>=321&&$_.m<=833){$k[$j++]=64}if($_.m>=834){$k[$j++]=32}var _6s=$k[--$j];$_.eccws=_6s;$_.m=$f($_.m+$_.eccws)+3;if($_.cccolumns>30){$_.cccolumns=30}for(;;){if($ceil($_.m/$_.cccolumns)<=30||$_.cccolumns>=30){break}$_.cccolumns=$_.cccolumns+1}$_.r=$ceil($_.m/$_.cccolumns);if($_.r<3){$_.r=3}var _76=$f($_.cccolumns*$_.r-$_.eccws)-3;$_.tgt=$f(~~(_76/5)*6+_76%5)*8;if($_.used<=8304){$k[$j++]=$f($_.tgt-$_.used)}else{$k[$j++]=-1}}var _7A=$k[$j-1];if(_7A==-1){$j--;if($eq($_.ccversion,"a")){$_.ccversion="b";$k[$j++]=$_.used;$_.rembits()}else{if($eq($_.ccversion,"b")&&$eq($_.lintype,"gs1-128")){$_.ccversion="c";$_.cccolumns=~~(($_.linwidth-52)/17);$k[$j++]=$_.used;$_.rembits()}else{$k[$j++]=-1}}}};$_.encode=function(){var _7H=$k[$j-1];if($ne(_7H,"raw")){var _7I=$k[--$j];var _7J=$k[--$j];$k[$j++]=$g(_7I,_7J)}else{$j--}var _7M=$k[--$j];$k[$j++]=Infinity;$F(_7M,function(){$k[$j-1]-=48});var _7O=$a();var _7P=_7O.length;$k[$j++]=_7O;$k[$j++]=_7P;if(_7P+$_.j>$_.gpfenc.length){$j-=2;$k[$j++]="bwipp.gs1ccDataTooLarge#36809";$k[$j++]="Data too large for the composite component";bwipp_raiseerror()}var _7S=$k[--$j];var _7T=$k[--$j];$P($_.gpfenc,$_.j,_7T);$_.j=$f(_7S+$_.j)};$k[$j++]=Infinity;for(var _7Y=0,_7Z=$_.gpf.length;_7Y<_7Z;_7Y++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=-1;$_.numericruns=$a();$k[$j++]=Infinity;for(var _7b=0,_7c=$_.gpf.length;_7b<_7c;_7b++){$k[$j++]=0}$k[$j++]=0;$_.alphanumericruns=$a();$k[$j++]=Infinity;for(var _7e=0,_7f=$_.gpf.length;_7e<_7f;_7e++){$k[$j++]=0}$k[$j++]=9999;$_.nextiso646only=$a();for(var _7h=$_.gpf.length-1;_7h>=0;_7h-=1){$_.i=_7h;var _7m=$Z($s(2),"00");var _7p=$g($_.gpf,$_.i);$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=_7m;var _7r=0;if(_7p==$_.gs1_cc_fnc1){_7p=94}$p(_7m,_7r,_7p);if($_.i<$_.gpf.length-1){var _7v=$k[$j-1];var _7y=$g($_.gpf,$_.i+1);var _80=1;if(_7y==$_.gs1_cc_fnc1){_7y=94}$p(_7v,_80,_7y)}var _83=$k[--$j];if($has($_.numeric,_83)){$p($_.numericruns,$_.i,$g($_.numericruns,$_.i+2)+2)}else{$p($_.numericruns,$_.i,0)}var _8C=$k[$j-1];var _8E=$has($_.alphanumeric,_8C);if(_8E){$p($_.alphanumericruns,$_.i,$g($_.alphanumericruns,$_.i+1)+1)}else{$p($_.alphanumericruns,$_.i,0)}var _8M=$k[--$j];if($has($_.iso646,_8M)&&!$has($_.alphanumeric,_8M)){$p($_.nextiso646only,$_.i,0)}else{$p($_.nextiso646only,$_.i,$g($_.nextiso646only,$_.i+1)+1)}}$_.gpfenc=$a(8304);$_.i=0;$_.j=0;for(;;){if($_.i==$_.gpf.length){break}for(;;){if($eq($_.mode,"numeric")){if($_.i<=$_.gpf.length-2){var _8e=$s(2);var _8h=$g($_.gpf,$_.i);$k[$j++]=_8e;var _8j=0;if(_8h==$_.gs1_cc_fnc1){_8h=94}$p(_8e,_8j,_8h);var _8l=$k[$j-1];var _8o=$g($_.gpf,$_.i+1);var _8q=1;if(_8o==$_.gs1_cc_fnc1){_8o=94}$p(_8l,_8q,_8o);var _8s=$k[$j-1];var _8u=$has($_.numeric,_8s);if(_8u){$k[$j++]=$_.numeric;$_.encode();$_.i=$_.i+2;break}$k[$j-1]=$_.gs1_cc_lalphanumeric;$k[$j++]=$_.numeric;$_.encode();$_.mode="alphanumeric";break}else{var _90=$g($_.gpf,$_.i);if(_90<48||_90>57){$k[$j++]=$_.gs1_cc_lalphanumeric;$k[$j++]=$_.numeric;$_.encode();$_.mode="alphanumeric";break}$k[$j++]="rem";$k[$j++]=$_.cdf.length+$_.j;$_.rembits();var _94=$k[--$j];var _95=$k[--$j];$_[_95]=_94;if($_.rem>=4&&$_.rem<=6){var _9B=$G($Z($s(6),"000000"),0,$_.rem);var _9G=$R($s(4),$g($_.gpf,$_.i)-47,2);$P(_9B,4-_9G.length,_9G);$k[$j++]=_9B;$k[$j++]="raw";$_.encode();$_.i=$_.i+1;break}else{var _9I=$s(2);$p(_9I,0,$g($_.gpf,$_.i));$p(_9I,1,94);$k[$j++]=_9I;$k[$j++]=$_.numeric;$_.encode();$_.i=$_.i+1;break}}}if($eq($_.mode,"alphanumeric")){if($g($_.gpf,$_.i)==$_.gs1_cc_fnc1){$k[$j++]=$_.gs1_cc_fnc1;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";$_.i=$_.i+1;break}var _9W=$g($_.gpf,$_.i);if($has($_.iso646,_9W)&&!$has($_.alphanumeric,_9W)){$k[$j++]=$_.gs1_cc_liso646;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="iso646";break}if($g($_.numericruns,$_.i)>=6){$k[$j++]=$_.gs1_cc_lnumeric;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";break}var _9i=$g($_.numericruns,$_.i);if(_9i>=4&&$f(_9i+$_.i)==$_.gpf.length){$k[$j++]=$_.gs1_cc_lnumeric;$k[$j++]=$_.alphanumeric;$_.encode();$_.mode="numeric";break}$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=$_.alphanumeric;$_.encode();$_.i=$_.i+1;break}if($eq($_.mode,"iso646")){if($g($_.gpf,$_.i)==$_.gs1_cc_fnc1){$k[$j++]=$_.gs1_cc_fnc1;$k[$j++]=$_.iso646;$_.encode();$_.mode="numeric";$_.i=$_.i+1;break}if($g($_.numericruns,$_.i)>=4&&$g($_.nextiso646only,$_.i)>=10){$k[$j++]=$_.gs1_cc_lnumeric;$k[$j++]=$_.iso646;$_.encode();$_.mode="numeric";break}if($g($_.alphanumericruns,$_.i)>=5&&$g($_.nextiso646only,$_.i)>=10){$k[$j++]=$_.gs1_cc_lalphanumeric;$k[$j++]=$_.iso646;$_.encode();$_.mode="alphanumeric";break}$k[$j++]=$g($_.gpf,$_.i);$k[$j++]=$_.iso646;$_.encode();$_.i=$_.i+1;break}}}$_.gpf=$G($_.gpfenc,0,$_.j);$k[$j++]=$_.cdf.length+$_.gpf.length;$_.rembits();var _AL=$k[$j-1];if(_AL==-1){$k[$j-1]="bwipp.gs1ccDataTooLarge#36946";$k[$j++]="Data too large for the composite component";bwipp_raiseerror()}var _AM=$k[--$j];$_.pad=$a(_AM);if($_.pad.length>0){for(var _AR=0,_AQ=$_.pad.length-1;_AR<=_AQ;_AR+=5){$_.i=_AR;var _AS=$_.pad;var _AT=$_.i;var _AU=$_.pad;var _AV=$_.i;var _AW=5;var _AX=_AU.length-_AV;if(_AU.length-_AV>5){var _=_AW;_AW=_AX;_AX=_}$P(_AS,_AT,$G($_.gs1_cc_fillpat,0,_AX))}if($eq($_.mode,"numeric")){$k[$j++]=Infinity;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.pad);var _Ab=$a();$_.pad=$G(_Ab,0,$_.pad.length)}if($eq($_.mode,"alpha")){$k[$j++]=Infinity;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=1;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.pad);var _Ag=$a();$_.pad=$G(_Ag,0,$_.pad.length)}}$k[$j++]=Infinity;$q($_.cdf);$q($_.gpf);$q($_.pad);$_.bits=$a();if($eq($_.ccversion,"a")){$k[$j++]=Infinity;for(var _An=0;_An<28;_An++){$k[$j++]=0}$_.cws=$a();$_.b=0;$_.c=0;for(;;){if($_.b==$_.bits.length){break}var _Aq=$_.bits;var _Ar=$_.b;var _As=$_.bits;var _At=$_.b;var _Au=_As.length-_At;var _Av=69;if(69>_As.length-_At){var _=_Au;_Au=_Av;_Av=_}$_.bs=$G(_Aq,_Ar,_Av);$_.bsl=$_.bs.length;$_.cs=$G($_.cws,$_.c,~~($_.bsl/10)+1);$_.csl=$_.cs.length;for(var _B5=0,_B4=$_.bsl-1;_B5<=_B4;_B5+=1){$_.i=_B5;for(var _B8=0,_B7=$_.csl-1;_B8<=_B7;_B8+=1){$_.j=_B8;var _B9=$_.cs;var _BA=$_.j;$p(_B9,_BA,$f($g(_B9,_BA)+$g($g($_.pwr928,$_.i),$_.j+7-$_.csl)*$g($_.bs,$_.bsl-$_.i-1)))}}for(var _BN=$_.csl-1;_BN>=1;_BN-=1){$_.i=_BN;var _BO=$_.cs;var _BP=$_.i;$p(_BO,_BP-1,$f($g(_BO,_BP-1)+~~($g($_.cs,$_.i)/928)));var _BU=$_.cs;var _BV=$_.i;$p(_BU,_BV,$g(_BU,_BV)%928)}$_.b=$_.b+$_.bsl;$_.c=$_.c+$_.csl}$_.cws=$G($_.cws,0,$_.c);$_.barcode=$s($_.cws.length*4);for(var _Bi=0,_Bh=$_.cws.length-1;_Bi<=_Bh;_Bi+=1){$_.i=_Bi;var _Bk=$Z($s(4),"^000");var _Bp=$R($s(4),$g($_.cws,$_.i),10);$P(_Bk,4-_Bp.length,_Bp);$P($_.barcode,$_.i*4,_Bk)}$del($_.options,"parse");$p($_.options,"cca",true);$p($_.options,"columns",$_.cccolumns);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_micropdf417();var _By=$k[--$j];$_.args=_By}if($eq($_.ccversion,"b")){$_.barcode=$s(~~($_.bits.length/8));for(var _C4=0,_C3=$_.barcode.length-1;_C4<=_C3;_C4+=1){$_.i=_C4;var _C7=$G($_.bits,$_.i*8,8);var _CC=0;for(var _C8=0,_C9=_C7.length;_C8<_C9;_C8++){_CC=$f(_CC+$g(_C7,_C8))*2}$p($_.barcode,$_.i,~~(_CC/2))}$del($_.options,"parse");$p($_.options,"ccb",true);$p($_.options,"columns",$_.cccolumns);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_micropdf417();var _CL=$k[--$j];$_.args=_CL}if($eq($_.ccversion,"c")){$_.barcode=$s(~~($_.bits.length/8));for(var _CR=0,_CQ=$_.barcode.length-1;_CR<=_CQ;_CR+=1){$_.i=_CR;var _CU=$G($_.bits,$_.i*8,8);var _CZ=0;for(var _CV=0,_CW=_CU.length;_CV<_CW;_CV++){_CZ=$f(_CZ+$g(_CU,_CV))*2}$p($_.barcode,$_.i,~~(_CZ/2))}$del($_.options,"parse");$p($_.options,"ccc",true);$p($_.options,"columns",$_.cccolumns);$p($_.options,"eclevel",~~($log($_.eccws)/$log(2))-1);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_pdf417();var _Ck=$k[--$j];$_.args=_Ck}$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_ean13composite(){var $__=$_;$_=Object.assign({},$_,bwipp_ean13composite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.guardwhitespace=false;$_.guardwidth=5;$_.guardheight=7;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#37123";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","ean13");$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _C=$s($_.comp.length+18);$P(_C,0,"(01)00000000000000");$P(_C,18,$_.comp);$k[$j++]=_C;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_ean13();var _G=$k[--$j];$_.linsym=_G;$_.sbs=$g($_.linsym,"sbs");$_.bhs=$g($_.linsym,"bhs");$_.bbs=$g($_.linsym,"bbs");$k[$j++]="linwidth";var _Q=0;$F($_.sbs,function(){var _O=$k[--$j];_Q=$f(_Q+$i(_O))});var _R=$k[--$j];$_[_R]=_Q;$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _U=$k[--$j];$_.compsym=_U;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;for(var _c=0,_d=$_.ccpixx-97;_c<_d;_c++){$k[$j++]=0}$_.linpad=$a();$k[$j++]=Infinity;var _e=$_.linsym;var _f=$has(_e,"txt");$k[$j++]=_e;$k[$j++]="txt";if(_f){var _g=$k[--$j];var _h=$k[--$j];$F($g(_h,_g),function(){var _j=$k[--$j];$q(_j);$r(4,-1);var _l=$k[--$j];$k[$j++]=$f(_l+($_.linpad.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _n=$_.linsym;var _o=$has(_n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_n;$k[$j++]="text1xalign";if(_o){var _p=$k[--$j];var _q=$k[--$j];$k[$j++]=$g(_q,_p)}else{$j-=2;$k[$j++]="unset"}var _s=$k[--$j];var _t=$k[--$j];$_[_t]=_s;var _u=$_.linsym;var _v=$has(_u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_u;$k[$j++]="text1yalign";if(_v){var _w=$k[--$j];var _x=$k[--$j];$k[$j++]=$g(_x,_w)}else{$j-=2;$k[$j++]="unset"}var _z=$k[--$j];var _10=$k[--$j];$_[_10]=_z;$_.diff=$f($_.linwidth+$_.linpad.length+1-$_.ccpixx);if($_.diff>0){$k[$j++]=Infinity;for(var _16=0,_17=$_.diff;_16<_17;_16++){$k[$j++]=0}$_.ccrpad=$a();$_.pixx=$f($_.ccpixx+$_.diff)}else{$_.ccrpad=$a(0);$_.pixx=$_.ccpixx}$_.ccrows=$_.ccrowmult.length;$_.nbars=~~(($_.sbs.length+1)/2);$_.bbsrow=$a($_.nbars);$_.bhsrow=$a($_.nbars);for(var _1K=0,_1J=$_.nbars-1;_1K<=_1J;_1K+=1){$_.b=_1K;$p($_.bbsrow,$_.b,~~($g($_.bbs,$_.b)*72));$p($_.bhsrow,$_.b,$f($g($_.bbsrow,$_.b)+$g($_.bhs,$_.b)*72))}var _1Y=$_.bhsrow;$k[$j++]="linheight";$k[$j++]=0;for(var _1Z=0,_1a=_1Y.length;_1Z<_1a;_1Z++){var _1b=$g(_1Y,_1Z);var _1c=$k[--$j];var _1d=$ceil(_1b);if(_1c<$ceil(_1b)){var _=_1d;_1d=_1c;_1c=_}$k[$j++]=_1c}var _1e=$k[--$j];var _1f=$k[--$j];$_[_1f]=_1e;var _1g=$_.bbsrow;$k[$j++]="lindescent";$k[$j++]=0;for(var _1h=0,_1i=_1g.length;_1h<_1i;_1h++){var _1j=$g(_1g,_1h);var _1k=$k[--$j];var _1l=$flr(_1j);if(_1k>$flr(_1j)){var _=_1l;_1l=_1k;_1k=_}$k[$j++]=_1k}var _1m=$k[--$j];var _1n=$k[--$j];$_[_1n]=_1m;var _1o=$_.txt;for(var _1p=0,_1q=_1o.length;_1p<_1q;_1p++){var _1r=$g(_1o,_1p);$p(_1r,2,$f($g(_1r,2)+-$_.lindescent))}$_.mainbars=$_.nbars;for(var _1x=0,_1w=$_.sbs.length-1;_1x<=_1w;_1x+=1){var _20=$t($g($_.sbs,_1x));$k[$j++]=_1x;if($eq(_20,"realtype")){var _21=$k[--$j];$_.mainbars=~~((_21+1)/2);break}else{$j--}}for(var _24=0,_23=$_.mainbars-1;_24<=_23;_24+=1){$p($_.bhsrow,_24,$_.linheight)}$_.rowbuf=$s($_.pixx);$_.prevrow="";$_.linrowmult=$a(72);$_.lri=0;$k[$j++]=Infinity;for(var _2D=$_.linheight-1,_2C=$_.lindescent;_2D>=_2C;_2D-=1){$_.y=_2D;$_.p=0;var _2E=$_.linpad;for(var _2F=0,_2G=_2E.length;_2F<_2G;_2F++){$p($_.rowbuf,$_.p,$g(_2E,_2F));$_.p=$_.p+1}$p($_.rowbuf,$_.p,0);$_.p=$_.p+1;for(var _2Q=0,_2P=$_.sbs.length-1;_2Q<=_2P;_2Q+=1){$_.i=_2Q;if($_.i%2==0){$k[$j++]=$g($_.bbsrow,~~($_.i/2))>$_.y||$g($_.bhsrow,~~($_.i/2))<$_.y?0:1}else{$k[$j++]=0}for(var _2e=0,_2f=$i($g($_.sbs,$_.i));_2e<_2f;_2e++){var _2i=$k[--$j];$p($_.rowbuf,$_.p,_2i);$_.p=$_.p+1;$k[$j++]=_2i}$j--}if($_.diff<0){$p($_.rowbuf,$_.p,0);$_.p=$_.p+1}if($eq($_.rowbuf,$_.prevrow)){var _2q=$_.linrowmult;var _2r=$_.lri;$p(_2q,_2r-1,$g(_2q,_2r-1)+1)}else{if($_.prevrow.length>0){var _2u=$_.prevrow;for(var _2v=0,_2w=_2u.length;_2v<_2w;_2v++){$k[$j++]=$g(_2u,_2v)}}$_.prevrow=$Z($s($_.pixx),$_.rowbuf);$p($_.linrowmult,$_.lri,1);$_.lri=$_.lri+1}}var _35=$_.prevrow;for(var _36=0,_37=_35.length;_36<_37;_36++){$k[$j++]=$g(_35,_36)}$_.linpixs=$a();$_.linrowmult=$G($_.linrowmult,0,$_.lri);$k[$j++]=Infinity;for(var _3F=0,_3G=$_.ccpixx,_3E=$_.ccpixs.length-1;_3G<0?_3F>=_3E:_3F<=_3E;_3F+=_3G){$_.i=_3F;$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3N=0;_3N<93;_3N++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpad);$k[$j++]=1;$k[$j++]=0;for(var _3Q=0;_3Q<93;_3Q++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=1;$q($_.ccrpad);$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3T=0;_3T<93;_3T++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=2;$k[$j++]=2;$k[$j++]=2;$q($_.linrowmult);$_.rowmult=$a();var _3Y=$_.rowmult;var _3d=0;for(var _3Z=0,_3a=_3Y.length;_3Z<_3a;_3Z++){_3d=$f(_3d+$g(_3Y,_3Z))}$_.pixy=_3d;var _3f=$_.linsym;var _3g=$has(_3f,"borderleft");$k[$j++]="borderleft";$k[$j++]=_3f;$k[$j++]="borderleft";if(_3g){var _3h=$k[--$j];var _3i=$k[--$j];$k[$j++]=$g(_3i,_3h)}else{$j-=2;$k[$j++]=10}var _3k=$_.linpad;var _3l=$k[--$j];var _3m=1;var _3n=$f(_3l-(_3k.length+1));if($f(_3l-(_3k.length+1))<1){var _=_3m;_3m=_3n;_3n=_}if(_3n<0){_3n=0}var _3p=$k[--$j];$_[_3p]=_3n;var _3q=$_.linsym;var _3r=$has(_3q,"borderright");$k[$j++]="borderright";$k[$j++]=_3q;$k[$j++]="borderright";if(_3r){var _3s=$k[--$j];var _3t=$k[--$j];$k[$j++]=$g(_3t,_3s)}else{$j-=2;$k[$j++]=10}var _3v=$_.pixx;var _3w=$_.linpad;var _3x=$_.linwidth;var _3y=$k[--$j];var _3z=$_.ccrpad;var _40=1-_3z.length;var _41=$f(_3y-(_3v-_3w.length-1-_3x));if($f(_3y-(_3v-_3w.length-1-_3x))<1-_3z.length){var _=_40;_40=_41;_41=_}if(_41<0){_41=0}var _43=$k[--$j];$_[_43]=_41;$_.bordertop=1;var _44=$_.linsym;var _45=$has(_44,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_44;$k[$j++]="borderbottom";if(_45){var _46=$k[--$j];var _47=$k[--$j];$k[$j++]=$g(_47,_46)}else{$j-=2;$k[$j++]=0}var _4A=$k[--$j];var _4B=$k[--$j];$_[_4B]=$f(_4A+$_.lindescent);var _4C=$_.linsym;var _4D=$has(_4C,"guardleftpos");$k[$j++]="guardleftpos";$k[$j++]=_4C;$k[$j++]="guardleftpos";if(_4D){var _4E=$k[--$j];var _4F=$k[--$j];$k[$j++]=$f($g(_4F,_4E)-($_.linpad.length+1))}else{$j-=2;$k[$j++]=0}var _4I=$k[--$j];var _4J=$k[--$j];$_[_4J]=_4I;var _4K=$_.linsym;var _4L=$has(_4K,"guardleftypos");$k[$j++]="guardleftypos";$k[$j++]=_4K;$k[$j++]="guardleftypos";if(_4L){var _4M=$k[--$j];var _4N=$k[--$j];$k[$j++]=$f($g(_4N,_4M)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4Q=$k[--$j];var _4R=$k[--$j];$_[_4R]=_4Q;var _4S=$_.linsym;var _4T=$has(_4S,"guardrightpos");$k[$j++]="guardrightpos";$k[$j++]=_4S;$k[$j++]="guardrightpos";if(_4T){var _4U=$k[--$j];var _4V=$k[--$j];$k[$j++]=$f($g(_4V,_4U)+($_.linpad.length+1+$_.linwidth-$_.pixx))}else{$j-=2;$k[$j++]=0}var _4a=$k[--$j];var _4b=$k[--$j];$_[_4b]=_4a;var _4c=$_.linsym;var _4d=$has(_4c,"guardrightypos");$k[$j++]="guardrightypos";$k[$j++]=_4c;$k[$j++]="guardrightypos";if(_4d){var _4e=$k[--$j];var _4f=$k[--$j];$k[$j++]=$f($g(_4f,_4e)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4i=$k[--$j];var _4j=$k[--$j];$_[_4j]=_4i;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["guardwhitespace",$_.guardwhitespace],["guardleftpos",$_.guardleftpos],["guardleftypos",$_.guardleftypos],["guardrightpos",$_.guardrightpos],["guardrightypos",$_.guardrightypos],["guardwidth",$_.guardwidth],["guardheight",$_.guardheight],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_ean8composite(){var $__=$_;$_=Object.assign({},$_,bwipp_ean8composite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.guardwhitespace=false;$_.guardwidth=5;$_.guardheight=7;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#37400";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","ean8");$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _C=$s($_.comp.length+18);$P(_C,0,"(01)00000000000000");$P(_C,18,$_.comp);$k[$j++]=_C;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_ean8();var _G=$k[--$j];$_.linsym=_G;$_.sbs=$g($_.linsym,"sbs");$_.bhs=$g($_.linsym,"bhs");$_.bbs=$g($_.linsym,"bbs");$k[$j++]="linwidth";var _Q=0;$F($_.sbs,function(){var _O=$k[--$j];_Q=$f(_Q+$i(_O))});var _R=$k[--$j];$_[_R]=_Q;$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _U=$k[--$j];$_.compsym=_U;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;for(var _c=0,_d=$_.ccpixx-69;_c<_d;_c++){$k[$j++]=0}$_.linpad=$a();$k[$j++]=Infinity;var _e=$_.linsym;var _f=$has(_e,"txt");$k[$j++]=_e;$k[$j++]="txt";if(_f){var _g=$k[--$j];var _h=$k[--$j];$F($g(_h,_g),function(){var _j=$k[--$j];$q(_j);$r(4,-1);var _l=$k[--$j];$k[$j++]=$f(_l+($_.linpad.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _n=$_.linsym;var _o=$has(_n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_n;$k[$j++]="text1xalign";if(_o){var _p=$k[--$j];var _q=$k[--$j];$k[$j++]=$g(_q,_p)}else{$j-=2;$k[$j++]="unset"}var _s=$k[--$j];var _t=$k[--$j];$_[_t]=_s;var _u=$_.linsym;var _v=$has(_u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_u;$k[$j++]="text1yalign";if(_v){var _w=$k[--$j];var _x=$k[--$j];$k[$j++]=$g(_x,_w)}else{$j-=2;$k[$j++]="unset"}var _z=$k[--$j];var _10=$k[--$j];$_[_10]=_z;$_.diff=$f($_.linwidth+$_.linpad.length+1-$_.ccpixx);if($_.diff>0){$k[$j++]=Infinity;for(var _16=0,_17=$_.diff;_16<_17;_16++){$k[$j++]=0}$_.ccrpad=$a();$_.pixx=$f($_.ccpixx+$_.diff)}else{$_.ccrpad=$a(0);$_.pixx=$_.ccpixx}$_.ccrows=$_.ccrowmult.length;$_.nbars=~~(($_.sbs.length+1)/2);$_.bbsrow=$a($_.nbars);$_.bhsrow=$a($_.nbars);for(var _1K=0,_1J=$_.nbars-1;_1K<=_1J;_1K+=1){$_.b=_1K;$p($_.bbsrow,$_.b,~~($g($_.bbs,$_.b)*72));$p($_.bhsrow,$_.b,$f($g($_.bbsrow,$_.b)+$g($_.bhs,$_.b)*72))}var _1Y=$_.bhsrow;$k[$j++]="linheight";$k[$j++]=0;for(var _1Z=0,_1a=_1Y.length;_1Z<_1a;_1Z++){var _1b=$g(_1Y,_1Z);var _1c=$k[--$j];var _1d=$ceil(_1b);if(_1c<$ceil(_1b)){var _=_1d;_1d=_1c;_1c=_}$k[$j++]=_1c}var _1e=$k[--$j];var _1f=$k[--$j];$_[_1f]=_1e;var _1g=$_.bbsrow;$k[$j++]="lindescent";$k[$j++]=0;for(var _1h=0,_1i=_1g.length;_1h<_1i;_1h++){var _1j=$g(_1g,_1h);var _1k=$k[--$j];var _1l=$flr(_1j);if(_1k>$flr(_1j)){var _=_1l;_1l=_1k;_1k=_}$k[$j++]=_1k}var _1m=$k[--$j];var _1n=$k[--$j];$_[_1n]=_1m;var _1o=$_.txt;for(var _1p=0,_1q=_1o.length;_1p<_1q;_1p++){var _1r=$g(_1o,_1p);$p(_1r,2,$f($g(_1r,2)+-$_.lindescent))}$_.mainbars=$_.nbars;for(var _1x=0,_1w=$_.sbs.length-1;_1x<=_1w;_1x+=1){var _20=$t($g($_.sbs,_1x));$k[$j++]=_1x;if($eq(_20,"realtype")){var _21=$k[--$j];$_.mainbars=~~((_21+1)/2);break}else{$j--}}for(var _24=0,_23=$_.mainbars-1;_24<=_23;_24+=1){$p($_.bhsrow,_24,$_.linheight)}$_.rowbuf=$s($_.pixx);$_.prevrow="";$_.linrowmult=$a(72);$_.lri=0;$k[$j++]=Infinity;for(var _2D=$_.linheight-1,_2C=$_.lindescent;_2D>=_2C;_2D-=1){$_.y=_2D;$_.p=0;var _2E=$_.linpad;for(var _2F=0,_2G=_2E.length;_2F<_2G;_2F++){$p($_.rowbuf,$_.p,$g(_2E,_2F));$_.p=$_.p+1}$p($_.rowbuf,$_.p,0);$_.p=$_.p+1;for(var _2Q=0,_2P=$_.sbs.length-1;_2Q<=_2P;_2Q+=1){$_.i=_2Q;if($_.i%2==0){$k[$j++]=$g($_.bbsrow,~~($_.i/2))>$_.y||$g($_.bhsrow,~~($_.i/2))<$_.y?0:1}else{$k[$j++]=0}for(var _2e=0,_2f=$i($g($_.sbs,$_.i));_2e<_2f;_2e++){var _2i=$k[--$j];$p($_.rowbuf,$_.p,_2i);$_.p=$_.p+1;$k[$j++]=_2i}$j--}if($_.diff<0){$p($_.rowbuf,$_.p,0);$_.p=$_.p+1}if($eq($_.rowbuf,$_.prevrow)){var _2q=$_.linrowmult;var _2r=$_.lri;$p(_2q,_2r-1,$g(_2q,_2r-1)+1)}else{if($_.prevrow.length>0){var _2u=$_.prevrow;for(var _2v=0,_2w=_2u.length;_2v<_2w;_2v++){$k[$j++]=$g(_2u,_2v)}}$_.prevrow=$Z($s($_.pixx),$_.rowbuf);$p($_.linrowmult,$_.lri,1);$_.lri=$_.lri+1}}var _35=$_.prevrow;for(var _36=0,_37=_35.length;_36<_37;_36++){$k[$j++]=$g(_35,_36)}$_.linpixs=$a();$_.linrowmult=$G($_.linrowmult,0,$_.lri);$k[$j++]=Infinity;for(var _3F=0,_3G=$_.ccpixx,_3E=$_.ccpixs.length-1;_3G<0?_3F>=_3E:_3F<=_3E;_3F+=_3G){$_.i=_3F;$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3N=0;_3N<65;_3N++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpad);$k[$j++]=1;$k[$j++]=0;for(var _3Q=0;_3Q<65;_3Q++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=1;$q($_.ccrpad);$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3T=0;_3T<65;_3T++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=2;$k[$j++]=2;$k[$j++]=2;$q($_.linrowmult);$_.rowmult=$a();var _3Y=$_.rowmult;var _3d=0;for(var _3Z=0,_3a=_3Y.length;_3Z<_3a;_3Z++){_3d=$f(_3d+$g(_3Y,_3Z))}$_.pixy=_3d;var _3f=$_.linsym;var _3g=$has(_3f,"borderleft");$k[$j++]="borderleft";$k[$j++]=_3f;$k[$j++]="borderleft";if(_3g){var _3h=$k[--$j];var _3i=$k[--$j];$k[$j++]=$g(_3i,_3h)}else{$j-=2;$k[$j++]=10}var _3k=$_.linpad;var _3l=$k[--$j];var _3m=1;var _3n=$f(_3l-(_3k.length+1));if($f(_3l-(_3k.length+1))<1){var _=_3m;_3m=_3n;_3n=_}if(_3n<0){_3n=0}var _3p=$k[--$j];$_[_3p]=_3n;var _3q=$_.linsym;var _3r=$has(_3q,"borderright");$k[$j++]="borderright";$k[$j++]=_3q;$k[$j++]="borderright";if(_3r){var _3s=$k[--$j];var _3t=$k[--$j];$k[$j++]=$g(_3t,_3s)}else{$j-=2;$k[$j++]=10}var _3v=$_.pixx;var _3w=$_.linpad;var _3x=$_.linwidth;var _3y=$k[--$j];var _3z=$_.ccrpad;var _40=1-_3z.length;var _41=$f(_3y-(_3v-_3w.length-1-_3x));if($f(_3y-(_3v-_3w.length-1-_3x))<1-_3z.length){var _=_40;_40=_41;_41=_}if(_41<0){_41=0}var _43=$k[--$j];$_[_43]=_41;$_.bordertop=1;var _44=$_.linsym;var _45=$has(_44,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_44;$k[$j++]="borderbottom";if(_45){var _46=$k[--$j];var _47=$k[--$j];$k[$j++]=$g(_47,_46)}else{$j-=2;$k[$j++]=0}var _4A=$k[--$j];var _4B=$k[--$j];$_[_4B]=$f(_4A+$_.lindescent);var _4C=$_.linsym;var _4D=$has(_4C,"guardleftpos");$k[$j++]="guardleftpos";$k[$j++]=_4C;$k[$j++]="guardleftpos";if(_4D){var _4E=$k[--$j];var _4F=$k[--$j];$k[$j++]=$f($g(_4F,_4E)-($_.linpad.length+1))}else{$j-=2;$k[$j++]=0}var _4I=$k[--$j];var _4J=$k[--$j];$_[_4J]=_4I;var _4K=$_.linsym;var _4L=$has(_4K,"guardleftypos");$k[$j++]="guardleftypos";$k[$j++]=_4K;$k[$j++]="guardleftypos";if(_4L){var _4M=$k[--$j];var _4N=$k[--$j];$k[$j++]=$f($g(_4N,_4M)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4Q=$k[--$j];var _4R=$k[--$j];$_[_4R]=_4Q;var _4S=$_.linsym;var _4T=$has(_4S,"guardrightpos");$k[$j++]="guardrightpos";$k[$j++]=_4S;$k[$j++]="guardrightpos";if(_4T){var _4U=$k[--$j];var _4V=$k[--$j];$k[$j++]=$f($g(_4V,_4U)+($_.linpad.length+1+$_.linwidth-$_.pixx))}else{$j-=2;$k[$j++]=0}var _4a=$k[--$j];var _4b=$k[--$j];$_[_4b]=_4a;var _4c=$_.linsym;var _4d=$has(_4c,"guardrightypos");$k[$j++]="guardrightypos";$k[$j++]=_4c;$k[$j++]="guardrightypos";if(_4d){var _4e=$k[--$j];var _4f=$k[--$j];$k[$j++]=$f($g(_4f,_4e)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4i=$k[--$j];var _4j=$k[--$j];$_[_4j]=_4i;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["guardwhitespace",$_.guardwhitespace],["guardleftpos",$_.guardleftpos],["guardleftypos",$_.guardleftypos],["guardrightpos",$_.guardrightpos],["guardrightypos",$_.guardrightypos],["guardwidth",$_.guardwidth],["guardheight",$_.guardheight],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_upcacomposite(){var $__=$_;$_=Object.assign({},$_,bwipp_upcacomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.guardwhitespace=false;$_.guardwidth=5;$_.guardheight=7;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#37673";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","ean13");$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _C=$s($_.comp.length+18);$P(_C,0,"(01)00000000000000");$P(_C,18,$_.comp);$k[$j++]=_C;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_upca();var _G=$k[--$j];$_.linsym=_G;$_.sbs=$g($_.linsym,"sbs");$_.bhs=$g($_.linsym,"bhs");$_.bbs=$g($_.linsym,"bbs");$k[$j++]="linwidth";var _Q=0;$F($_.sbs,function(){var _O=$k[--$j];_Q=$f(_Q+$i(_O))});var _R=$k[--$j];$_[_R]=_Q;$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _U=$k[--$j];$_.compsym=_U;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;for(var _c=0,_d=$_.ccpixx-97;_c<_d;_c++){$k[$j++]=0}$_.linpad=$a();$k[$j++]=Infinity;var _e=$_.linsym;var _f=$has(_e,"txt");$k[$j++]=_e;$k[$j++]="txt";if(_f){var _g=$k[--$j];var _h=$k[--$j];$F($g(_h,_g),function(){var _j=$k[--$j];$q(_j);$r(4,-1);var _l=$k[--$j];$k[$j++]=$f(_l+($_.linpad.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _n=$_.linsym;var _o=$has(_n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_n;$k[$j++]="text1xalign";if(_o){var _p=$k[--$j];var _q=$k[--$j];$k[$j++]=$g(_q,_p)}else{$j-=2;$k[$j++]="unset"}var _s=$k[--$j];var _t=$k[--$j];$_[_t]=_s;var _u=$_.linsym;var _v=$has(_u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_u;$k[$j++]="text1yalign";if(_v){var _w=$k[--$j];var _x=$k[--$j];$k[$j++]=$g(_x,_w)}else{$j-=2;$k[$j++]="unset"}var _z=$k[--$j];var _10=$k[--$j];$_[_10]=_z;$_.diff=$f($_.linwidth+$_.linpad.length+1-$_.ccpixx);if($_.diff>0){$k[$j++]=Infinity;for(var _16=0,_17=$_.diff;_16<_17;_16++){$k[$j++]=0}$_.ccrpad=$a();$_.pixx=$f($_.ccpixx+$_.diff)}else{$_.ccrpad=$a(0);$_.pixx=$_.ccpixx}$_.ccrows=$_.ccrowmult.length;$_.nbars=~~(($_.sbs.length+1)/2);$_.bbsrow=$a($_.nbars);$_.bhsrow=$a($_.nbars);for(var _1K=0,_1J=$_.nbars-1;_1K<=_1J;_1K+=1){$_.b=_1K;$p($_.bbsrow,$_.b,~~($g($_.bbs,$_.b)*72));$p($_.bhsrow,$_.b,$f($g($_.bbsrow,$_.b)+$g($_.bhs,$_.b)*72))}var _1Y=$_.bhsrow;$k[$j++]="linheight";$k[$j++]=0;for(var _1Z=0,_1a=_1Y.length;_1Z<_1a;_1Z++){var _1b=$g(_1Y,_1Z);var _1c=$k[--$j];var _1d=$ceil(_1b);if(_1c<$ceil(_1b)){var _=_1d;_1d=_1c;_1c=_}$k[$j++]=_1c}var _1e=$k[--$j];var _1f=$k[--$j];$_[_1f]=_1e;var _1g=$_.bbsrow;$k[$j++]="lindescent";$k[$j++]=0;for(var _1h=0,_1i=_1g.length;_1h<_1i;_1h++){var _1j=$g(_1g,_1h);var _1k=$k[--$j];var _1l=$flr(_1j);if(_1k>$flr(_1j)){var _=_1l;_1l=_1k;_1k=_}$k[$j++]=_1k}var _1m=$k[--$j];var _1n=$k[--$j];$_[_1n]=_1m;var _1o=$_.txt;for(var _1p=0,_1q=_1o.length;_1p<_1q;_1p++){var _1r=$g(_1o,_1p);$p(_1r,2,$f($g(_1r,2)+-$_.lindescent))}$_.mainbars=$_.nbars;for(var _1x=0,_1w=$_.sbs.length-1;_1x<=_1w;_1x+=1){var _20=$t($g($_.sbs,_1x));$k[$j++]=_1x;if($eq(_20,"realtype")){var _21=$k[--$j];$_.mainbars=~~((_21+1)/2);break}else{$j--}}for(var _24=0,_23=$_.mainbars-1;_24<=_23;_24+=1){$p($_.bhsrow,_24,$_.linheight)}$_.rowbuf=$s($_.pixx);$_.prevrow="";$_.linrowmult=$a(72);$_.lri=0;$k[$j++]=Infinity;for(var _2D=$_.linheight-1,_2C=$_.lindescent;_2D>=_2C;_2D-=1){$_.y=_2D;$_.p=0;var _2E=$_.linpad;for(var _2F=0,_2G=_2E.length;_2F<_2G;_2F++){$p($_.rowbuf,$_.p,$g(_2E,_2F));$_.p=$_.p+1}$p($_.rowbuf,$_.p,0);$_.p=$_.p+1;for(var _2Q=0,_2P=$_.sbs.length-1;_2Q<=_2P;_2Q+=1){$_.i=_2Q;if($_.i%2==0){$k[$j++]=$g($_.bbsrow,~~($_.i/2))>$_.y||$g($_.bhsrow,~~($_.i/2))<$_.y?0:1}else{$k[$j++]=0}for(var _2e=0,_2f=$i($g($_.sbs,$_.i));_2e<_2f;_2e++){var _2i=$k[--$j];$p($_.rowbuf,$_.p,_2i);$_.p=$_.p+1;$k[$j++]=_2i}$j--}if($_.diff<0){$p($_.rowbuf,$_.p,0);$_.p=$_.p+1}if($eq($_.rowbuf,$_.prevrow)){var _2q=$_.linrowmult;var _2r=$_.lri;$p(_2q,_2r-1,$g(_2q,_2r-1)+1)}else{if($_.prevrow.length>0){var _2u=$_.prevrow;for(var _2v=0,_2w=_2u.length;_2v<_2w;_2v++){$k[$j++]=$g(_2u,_2v)}}$_.prevrow=$Z($s($_.pixx),$_.rowbuf);$p($_.linrowmult,$_.lri,1);$_.lri=$_.lri+1}}var _35=$_.prevrow;for(var _36=0,_37=_35.length;_36<_37;_36++){$k[$j++]=$g(_35,_36)}$_.linpixs=$a();$_.linrowmult=$G($_.linrowmult,0,$_.lri);$k[$j++]=Infinity;for(var _3F=0,_3G=$_.ccpixx,_3E=$_.ccpixs.length-1;_3G<0?_3F>=_3E:_3F<=_3E;_3F+=_3G){$_.i=_3F;$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3N=0;_3N<93;_3N++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpad);$k[$j++]=1;$k[$j++]=0;for(var _3Q=0;_3Q<93;_3Q++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=1;$q($_.ccrpad);$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3T=0;_3T<93;_3T++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=2;$k[$j++]=2;$k[$j++]=2;$q($_.linrowmult);$_.rowmult=$a();var _3Y=$_.rowmult;var _3d=0;for(var _3Z=0,_3a=_3Y.length;_3Z<_3a;_3Z++){_3d=$f(_3d+$g(_3Y,_3Z))}$_.pixy=_3d;var _3f=$_.linsym;var _3g=$has(_3f,"borderleft");$k[$j++]="borderleft";$k[$j++]=_3f;$k[$j++]="borderleft";if(_3g){var _3h=$k[--$j];var _3i=$k[--$j];$k[$j++]=$g(_3i,_3h)}else{$j-=2;$k[$j++]=10}var _3k=$_.linpad;var _3l=$k[--$j];var _3m=1;var _3n=$f(_3l-(_3k.length+1));if($f(_3l-(_3k.length+1))<1){var _=_3m;_3m=_3n;_3n=_}if(_3n<0){_3n=0}var _3p=$k[--$j];$_[_3p]=_3n;var _3q=$_.linsym;var _3r=$has(_3q,"borderright");$k[$j++]="borderright";$k[$j++]=_3q;$k[$j++]="borderright";if(_3r){var _3s=$k[--$j];var _3t=$k[--$j];$k[$j++]=$g(_3t,_3s)}else{$j-=2;$k[$j++]=10}var _3v=$_.pixx;var _3w=$_.linpad;var _3x=$_.linwidth;var _3y=$k[--$j];var _3z=$_.ccrpad;var _40=1-_3z.length;var _41=$f(_3y-(_3v-_3w.length-1-_3x));if($f(_3y-(_3v-_3w.length-1-_3x))<1-_3z.length){var _=_40;_40=_41;_41=_}if(_41<0){_41=0}var _43=$k[--$j];$_[_43]=_41;$_.bordertop=1;var _44=$_.linsym;var _45=$has(_44,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_44;$k[$j++]="borderbottom";if(_45){var _46=$k[--$j];var _47=$k[--$j];$k[$j++]=$g(_47,_46)}else{$j-=2;$k[$j++]=0}var _4A=$k[--$j];var _4B=$k[--$j];$_[_4B]=$f(_4A+$_.lindescent);var _4C=$_.linsym;var _4D=$has(_4C,"guardleftpos");$k[$j++]="guardleftpos";$k[$j++]=_4C;$k[$j++]="guardleftpos";if(_4D){var _4E=$k[--$j];var _4F=$k[--$j];$k[$j++]=$f($g(_4F,_4E)-($_.linpad.length+1))}else{$j-=2;$k[$j++]=0}var _4I=$k[--$j];var _4J=$k[--$j];$_[_4J]=_4I;var _4K=$_.linsym;var _4L=$has(_4K,"guardleftypos");$k[$j++]="guardleftypos";$k[$j++]=_4K;$k[$j++]="guardleftypos";if(_4L){var _4M=$k[--$j];var _4N=$k[--$j];$k[$j++]=$f($g(_4N,_4M)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4Q=$k[--$j];var _4R=$k[--$j];$_[_4R]=_4Q;var _4S=$_.linsym;var _4T=$has(_4S,"guardrightpos");$k[$j++]="guardrightpos";$k[$j++]=_4S;$k[$j++]="guardrightpos";if(_4T){var _4U=$k[--$j];var _4V=$k[--$j];$k[$j++]=$f($g(_4V,_4U)+($_.linpad.length+1+$_.linwidth-$_.pixx))}else{$j-=2;$k[$j++]=0}var _4a=$k[--$j];var _4b=$k[--$j];$_[_4b]=_4a;var _4c=$_.linsym;var _4d=$has(_4c,"guardrightypos");$k[$j++]="guardrightypos";$k[$j++]=_4c;$k[$j++]="guardrightypos";if(_4d){var _4e=$k[--$j];var _4f=$k[--$j];$k[$j++]=$f($g(_4f,_4e)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4i=$k[--$j];var _4j=$k[--$j];$_[_4j]=_4i;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["guardwhitespace",$_.guardwhitespace],["guardleftpos",$_.guardleftpos],["guardleftypos",$_.guardleftypos],["guardrightpos",$_.guardrightpos],["guardrightypos",$_.guardrightypos],["guardwidth",$_.guardwidth],["guardheight",$_.guardheight],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_upcecomposite(){var $__=$_;$_=Object.assign({},$_,bwipp_upcecomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.guardwhitespace=false;$_.guardwidth=5;$_.guardheight=7;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#37943";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","upce");$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _C=$s($_.comp.length+18);$P(_C,0,"(01)00000000000000");$P(_C,18,$_.comp);$k[$j++]=_C;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_upce();var _G=$k[--$j];$_.linsym=_G;$_.sbs=$g($_.linsym,"sbs");$_.bhs=$g($_.linsym,"bhs");$_.bbs=$g($_.linsym,"bbs");$k[$j++]="linwidth";var _Q=0;$F($_.sbs,function(){var _O=$k[--$j];_Q=$f(_Q+$i(_O))});var _R=$k[--$j];$_[_R]=_Q;$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _U=$k[--$j];$_.compsym=_U;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;for(var _c=0,_d=$_.ccpixx-53;_c<_d;_c++){$k[$j++]=0}$_.linpad=$a();$k[$j++]=Infinity;var _e=$_.linsym;var _f=$has(_e,"txt");$k[$j++]=_e;$k[$j++]="txt";if(_f){var _g=$k[--$j];var _h=$k[--$j];$F($g(_h,_g),function(){var _j=$k[--$j];$q(_j);$r(4,-1);var _l=$k[--$j];$k[$j++]=$f(_l+($_.linpad.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _n=$_.linsym;var _o=$has(_n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_n;$k[$j++]="text1xalign";if(_o){var _p=$k[--$j];var _q=$k[--$j];$k[$j++]=$g(_q,_p)}else{$j-=2;$k[$j++]="unset"}var _s=$k[--$j];var _t=$k[--$j];$_[_t]=_s;var _u=$_.linsym;var _v=$has(_u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_u;$k[$j++]="text1yalign";if(_v){var _w=$k[--$j];var _x=$k[--$j];$k[$j++]=$g(_x,_w)}else{$j-=2;$k[$j++]="unset"}var _z=$k[--$j];var _10=$k[--$j];$_[_10]=_z;$_.diff=$f($_.linwidth+$_.linpad.length+1-$_.ccpixx);if($_.diff>0){$k[$j++]=Infinity;for(var _16=0,_17=$_.diff;_16<_17;_16++){$k[$j++]=0}$_.ccrpad=$a();$_.pixx=$f($_.ccpixx+$_.diff)}else{$_.ccrpad=$a(0);$_.pixx=$_.ccpixx}$_.ccrows=$_.ccrowmult.length;$_.nbars=~~(($_.sbs.length+1)/2);$_.bbsrow=$a($_.nbars);$_.bhsrow=$a($_.nbars);for(var _1K=0,_1J=$_.nbars-1;_1K<=_1J;_1K+=1){$_.b=_1K;$p($_.bbsrow,$_.b,~~($g($_.bbs,$_.b)*72));$p($_.bhsrow,$_.b,$f($g($_.bbsrow,$_.b)+$g($_.bhs,$_.b)*72))}var _1Y=$_.bhsrow;$k[$j++]="linheight";$k[$j++]=0;for(var _1Z=0,_1a=_1Y.length;_1Z<_1a;_1Z++){var _1b=$g(_1Y,_1Z);var _1c=$k[--$j];var _1d=$ceil(_1b);if(_1c<$ceil(_1b)){var _=_1d;_1d=_1c;_1c=_}$k[$j++]=_1c}var _1e=$k[--$j];var _1f=$k[--$j];$_[_1f]=_1e;var _1g=$_.bbsrow;$k[$j++]="lindescent";$k[$j++]=0;for(var _1h=0,_1i=_1g.length;_1h<_1i;_1h++){var _1j=$g(_1g,_1h);var _1k=$k[--$j];var _1l=$flr(_1j);if(_1k>$flr(_1j)){var _=_1l;_1l=_1k;_1k=_}$k[$j++]=_1k}var _1m=$k[--$j];var _1n=$k[--$j];$_[_1n]=_1m;var _1o=$_.txt;for(var _1p=0,_1q=_1o.length;_1p<_1q;_1p++){var _1r=$g(_1o,_1p);$p(_1r,2,$f($g(_1r,2)+-$_.lindescent))}$_.mainbars=$_.nbars;for(var _1x=0,_1w=$_.sbs.length-1;_1x<=_1w;_1x+=1){var _20=$t($g($_.sbs,_1x));$k[$j++]=_1x;if($eq(_20,"realtype")){var _21=$k[--$j];$_.mainbars=~~((_21+1)/2);break}else{$j--}}for(var _24=0,_23=$_.mainbars-1;_24<=_23;_24+=1){$p($_.bhsrow,_24,$_.linheight)}$_.rowbuf=$s($_.pixx);$_.prevrow="";$_.linrowmult=$a(72);$_.lri=0;$k[$j++]=Infinity;for(var _2D=$_.linheight-1,_2C=$_.lindescent;_2D>=_2C;_2D-=1){$_.y=_2D;$_.p=0;var _2E=$_.linpad;for(var _2F=0,_2G=_2E.length;_2F<_2G;_2F++){$p($_.rowbuf,$_.p,$g(_2E,_2F));$_.p=$_.p+1}$p($_.rowbuf,$_.p,0);$_.p=$_.p+1;for(var _2Q=0,_2P=$_.sbs.length-1;_2Q<=_2P;_2Q+=1){$_.i=_2Q;if($_.i%2==0){$k[$j++]=$g($_.bbsrow,~~($_.i/2))>$_.y||$g($_.bhsrow,~~($_.i/2))<$_.y?0:1}else{$k[$j++]=0}for(var _2e=0,_2f=$i($g($_.sbs,$_.i));_2e<_2f;_2e++){var _2i=$k[--$j];$p($_.rowbuf,$_.p,_2i);$_.p=$_.p+1;$k[$j++]=_2i}$j--}if($_.diff<0){$p($_.rowbuf,$_.p,0);$_.p=$_.p+1}if($eq($_.rowbuf,$_.prevrow)){var _2q=$_.linrowmult;var _2r=$_.lri;$p(_2q,_2r-1,$g(_2q,_2r-1)+1)}else{if($_.prevrow.length>0){var _2u=$_.prevrow;for(var _2v=0,_2w=_2u.length;_2v<_2w;_2v++){$k[$j++]=$g(_2u,_2v)}}$_.prevrow=$Z($s($_.pixx),$_.rowbuf);$p($_.linrowmult,$_.lri,1);$_.lri=$_.lri+1}}var _35=$_.prevrow;for(var _36=0,_37=_35.length;_36<_37;_36++){$k[$j++]=$g(_35,_36)}$_.linpixs=$a();$_.linrowmult=$G($_.linrowmult,0,$_.lri);$k[$j++]=Infinity;for(var _3F=0,_3G=$_.ccpixx,_3E=$_.ccpixs.length-1;_3G<0?_3F>=_3E:_3F<=_3E;_3F+=_3G){$_.i=_3F;$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3N=0;_3N<49;_3N++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpad);$k[$j++]=1;$k[$j++]=0;for(var _3Q=0;_3Q<49;_3Q++){$k[$j++]=0}$k[$j++]=0;$k[$j++]=1;$q($_.ccrpad);$q($_.linpad);$k[$j++]=0;$k[$j++]=1;for(var _3T=0;_3T<49;_3T++){$k[$j++]=0}$k[$j++]=1;$k[$j++]=0;$q($_.ccrpad);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=2;$k[$j++]=2;$k[$j++]=2;$q($_.linrowmult);$_.rowmult=$a();var _3Y=$_.rowmult;var _3d=0;for(var _3Z=0,_3a=_3Y.length;_3Z<_3a;_3Z++){_3d=$f(_3d+$g(_3Y,_3Z))}$_.pixy=_3d;var _3f=$_.linsym;var _3g=$has(_3f,"borderleft");$k[$j++]="borderleft";$k[$j++]=_3f;$k[$j++]="borderleft";if(_3g){var _3h=$k[--$j];var _3i=$k[--$j];$k[$j++]=$g(_3i,_3h)}else{$j-=2;$k[$j++]=10}var _3k=$_.linpad;var _3l=$k[--$j];var _3m=1;var _3n=$f(_3l-(_3k.length+1));if($f(_3l-(_3k.length+1))<1){var _=_3m;_3m=_3n;_3n=_}if(_3n<0){_3n=0}var _3p=$k[--$j];$_[_3p]=_3n;var _3q=$_.linsym;var _3r=$has(_3q,"borderright");$k[$j++]="borderright";$k[$j++]=_3q;$k[$j++]="borderright";if(_3r){var _3s=$k[--$j];var _3t=$k[--$j];$k[$j++]=$g(_3t,_3s)}else{$j-=2;$k[$j++]=10}var _3v=$_.pixx;var _3w=$_.linpad;var _3x=$_.linwidth;var _3y=$k[--$j];var _3z=$_.ccrpad;var _40=1-_3z.length;var _41=$f(_3y-(_3v-_3w.length-1-_3x));if($f(_3y-(_3v-_3w.length-1-_3x))<1-_3z.length){var _=_40;_40=_41;_41=_}if(_41<0){_41=0}var _43=$k[--$j];$_[_43]=_41;$_.bordertop=1;var _44=$_.linsym;var _45=$has(_44,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_44;$k[$j++]="borderbottom";if(_45){var _46=$k[--$j];var _47=$k[--$j];$k[$j++]=$g(_47,_46)}else{$j-=2;$k[$j++]=0}var _4A=$k[--$j];var _4B=$k[--$j];$_[_4B]=$f(_4A+$_.lindescent);var _4C=$_.linsym;var _4D=$has(_4C,"guardleftpos");$k[$j++]="guardleftpos";$k[$j++]=_4C;$k[$j++]="guardleftpos";if(_4D){var _4E=$k[--$j];var _4F=$k[--$j];$k[$j++]=$f($g(_4F,_4E)-($_.linpad.length+1))}else{$j-=2;$k[$j++]=0}var _4I=$k[--$j];var _4J=$k[--$j];$_[_4J]=_4I;var _4K=$_.linsym;var _4L=$has(_4K,"guardleftypos");$k[$j++]="guardleftypos";$k[$j++]=_4K;$k[$j++]="guardleftypos";if(_4L){var _4M=$k[--$j];var _4N=$k[--$j];$k[$j++]=$f($g(_4N,_4M)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4Q=$k[--$j];var _4R=$k[--$j];$_[_4R]=_4Q;var _4S=$_.linsym;var _4T=$has(_4S,"guardrightpos");$k[$j++]="guardrightpos";$k[$j++]=_4S;$k[$j++]="guardrightpos";if(_4T){var _4U=$k[--$j];var _4V=$k[--$j];$k[$j++]=$f($g(_4V,_4U)+($_.linpad.length+1+$_.linwidth-$_.pixx))}else{$j-=2;$k[$j++]=0}var _4a=$k[--$j];var _4b=$k[--$j];$_[_4b]=_4a;var _4c=$_.linsym;var _4d=$has(_4c,"guardrightypos");$k[$j++]="guardrightypos";$k[$j++]=_4c;$k[$j++]="guardrightypos";if(_4d){var _4e=$k[--$j];var _4f=$k[--$j];$k[$j++]=$f($g(_4f,_4e)+-$_.lindescent)}else{$j-=2;$k[$j++]=0}var _4i=$k[--$j];var _4j=$k[--$j];$_[_4j]=_4i;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["guardwhitespace",$_.guardwhitespace],["guardleftpos",$_.guardleftpos],["guardleftypos",$_.guardleftypos],["guardrightpos",$_.guardrightpos],["guardrightypos",$_.guardrightypos],["guardwidth",$_.guardwidth],["guardheight",$_.guardheight],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databaromnicomposite(){if(!bwipp_databaromnicomposite.globals){var $__=$_;$_=bwipp_databaromnicomposite.globals={};$_.databaromnicomposite_seppad=$a([0,0,0,0]);$_.databaromnicomposite_findersep=$a([0,0,0,0,0,0,0,0,0,0,1,0,0]);$_.databaromnicomposite_f3pat=$a([1,1,1,1,1,1,1,1,1,0,1,1,1]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databaromnicomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _3=$k[--$j];$_.options=_3;var _4=$k[--$j];$_.barcode=_4;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _8=$k[--$j];if(_8){var _9=$k[--$j];$_.linear=_9;$j--;var _A=$k[--$j];$_.comp=_A}else{$k[$j-1]="bwipp.missingCompositeComponent#38215";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databaromni");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _H=$s($_.linear.length+$_.comp.length);$P(_H,0,$_.linear);$P(_H,$_.linear.length,$_.comp);$k[$j++]=_H;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databaromni();var _N=$k[--$j];$_.linsbs=$g(_N,"sbs");$_.linheight=$g($g(_N,"bhs"),0)*72;$_.linsym=_N;$_.sepfinder=function(){var _R=$k[--$j];$_.fp=_R;for(var _V=$_.fp,_U=$_.fp+12;_V<=_U;_V+=1){$_.i=_V;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _h=$k[--$j];$p($_.sep,$_.i,_h)}var _p=true;for(var _j=0;_j<=12;_j+=1){var _m=$g($_.bot,$f(_j+$_.fp));_p=_p&&$eq(_m,$g($_.databaromnicomposite_f3pat,_j))}if(_p){$P($_.sep,$_.fp,$_.databaromnicomposite_findersep)}};$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _t=$k[--$j];var _u=$k[$j-1];var _v=_u==1?0:1;for(var _w=0,_x=_t;_w<_x;_w++){$k[$j++]=_v}});$as($a($m()-1));var _10=$k[--$j];$_.bot=_10;$j-=2;$k[$j++]=Infinity;$F($_.bot,function(){var _12=$k[--$j];$k[$j++]=1-_12});$_.sep=$a();$P($_.sep,0,$a([0,0,0]));$P($_.sep,$_.sep.length-4,$_.databaromnicomposite_seppad);$k[$j++]=18;$_.sepfinder();$k[$j++]=64;$_.sepfinder();$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _19=$k[--$j];$_.compsym=_19;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _1H=$k[--$j];var _1I=$k[$j-1];var _1J=_1I==0?1:0;for(var _1K=0,_1L=$i(_1H);_1K<_1L;_1K++){$k[$j++]=_1J}});$_.linpixs=$a();$k[$j++]=Infinity;$k[$j++]=0;$q($_.sep);$_.sep=$a();$_.linheight=~~$_.linheight;$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _1S=0,_1T=$_.ccpixx,_1R=$_.ccpixs.length-1;_1T<0?_1S>=_1R:_1S<=_1R;_1S+=_1T){$_.i=_1S;$q($G($_.ccpixs,$_.i,$_.ccpixx));$k[$j++]=0}$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.sep);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$k[$j++]=$_.linheight;$_.rowmult=$a();$_.pixx=$_.ccpixx+1;$k[$j++]=Infinity;var _1d=$_.linsym;var _1e=$has(_1d,"txt");$k[$j++]=_1d;$k[$j++]="txt";if(_1e){var _1f=$k[--$j];var _1g=$k[--$j];$F($g(_1g,_1f),function(){var _1i=$k[--$j];$q(_1i);$r(4,-1);var _1l=$k[--$j];$k[$j++]=$f(_1l+($_.pixx-$_.linpixs.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1n=$_.linsym;var _1o=$has(_1n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1n;$k[$j++]="text1xalign";if(_1o){var _1p=$k[--$j];var _1q=$k[--$j];$k[$j++]=$g(_1q,_1p)}else{$j-=2;$k[$j++]="unset"}var _1s=$k[--$j];var _1t=$k[--$j];$_[_1t]=_1s;var _1u=$_.linsym;var _1v=$has(_1u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1u;$k[$j++]="text1yalign";if(_1v){var _1w=$k[--$j];var _1x=$k[--$j];$k[$j++]=$g(_1x,_1w)}else{$j-=2;$k[$j++]="unset"}var _1z=$k[--$j];var _20=$k[--$j];$_[_20]=_1z;var _21=$_.rowmult;var _26=0;for(var _22=0,_23=_21.length;_22<_23;_22++){_26=$f(_26+$g(_21,_22))}$_.pixy=_26;var _28=$_.linsym;var _29=$has(_28,"borderleft");$k[$j++]="borderleft";$k[$j++]=_28;$k[$j++]="borderleft";if(_29){var _2A=$k[--$j];var _2B=$k[--$j];$k[$j++]=$g(_2B,_2A)}else{$j-=2;$k[$j++]=10}var _2D=$_.pixx;var _2E=$_.linpixs;var _2F=$k[--$j];var _2G=1;var _2H=$f(_2F-(_2D-_2E.length+1));if($f(_2F-(_2D-_2E.length+1))<1){var _=_2G;_2G=_2H;_2H=_}if(_2H<0){_2H=0}var _2J=$k[--$j];$_[_2J]=_2H;var _2K=$_.linsym;var _2L=$has(_2K,"borderright");$k[$j++]="borderright";$k[$j++]=_2K;$k[$j++]="borderright";if(_2L){var _2M=$k[--$j];var _2N=$k[--$j];$k[$j++]=$g(_2N,_2M)}else{$j-=2;$k[$j++]=10}var _2P=$k[--$j];var _2Q=0;if(_2P<0){var _=_2Q;_2Q=_2P;_2P=_}if(_2P<0){_2P=0}var _2S=$k[--$j];$_[_2S]=_2P;$_.bordertop=1;var _2T=$_.linsym;var _2U=$has(_2T,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2T;$k[$j++]="borderbottom";if(_2U){var _2V=$k[--$j];var _2W=$k[--$j];$k[$j++]=$g(_2W,_2V)}else{$j-=2;$k[$j++]=0}var _2Y=$k[--$j];var _2Z=$k[--$j];$_[_2Z]=_2Y;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarstackedcomposite(){if(!bwipp_databarstackedcomposite.globals){var $__=$_;$_=bwipp_databarstackedcomposite.globals={};$_.databarstackedcomposite_seppad=$a([0,0,0,0]);$_.databarstackedcomposite_findersep=$a([0,0,0,0,0,0,0,0,0,0,1,0,0]);$_.databarstackedcomposite_f3pat=$a([1,1,1,1,1,1,1,1,1,0,1,1,1]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarstackedcomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _3=$k[--$j];$_.options=_3;var _4=$k[--$j];$_.barcode=_4;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _8=$k[--$j];if(_8){var _9=$k[--$j];$_.linear=_9;$j--;var _A=$k[--$j];$_.comp=_A}else{$k[$j-1]="bwipp.missingCompositeComponent#38449";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databarstacked");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _H=$s($_.linear.length+$_.comp.length);$P(_H,0,$_.linear);$P(_H,$_.linear.length,$_.comp);$k[$j++]=_H;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databarstacked();var _N=$k[--$j];$_.bot=$G($g(_N,"pixs"),0,$g(_N,"pixx"));$_.linheight=$g(_N,"pixy");$_.linsym=_N;$_.linpixs=$g($_.linsym,"pixs");$_.linrowmult=$g($_.linsym,"rowmult");$_.sepfinder=function(){var _W=$k[--$j];$_.fp=_W;for(var _a=$_.fp,_Z=$_.fp+12;_a<=_Z;_a+=1){$_.i=_a;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _m=$k[--$j];$p($_.sep,$_.i,_m)}var _u=true;for(var _o=0;_o<=12;_o+=1){var _r=$g($_.bot,$f(_o+$_.fp));_u=_u&&$eq(_r,$g($_.databarstackedcomposite_f3pat,_o))}if(_u){$P($_.sep,$_.fp,$_.databarstackedcomposite_findersep)}};$k[$j++]=Infinity;var _x=$_.bot;for(var _y=0,_z=_x.length;_y<_z;_y++){$k[$j++]=1-$g(_x,_y)}$_.sep=$a();$P($_.sep,0,$_.databarstackedcomposite_seppad);$P($_.sep,$_.sep.length-4,$_.databarstackedcomposite_seppad);$k[$j++]=18;$_.sepfinder();$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _16=$k[--$j];$_.compsym=_16;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$_.linwidth=$_.sep.length;$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _1I=0,_1J=$_.ccpixx,_1H=$_.ccpixs.length-1;_1J<0?_1I>=_1H:_1I<=_1H;_1I+=_1J){$_.i=_1I;$k[$j++]=0;$q($G($_.ccpixs,$_.i,$_.ccpixx))}$q($_.sep);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;for(var _1S=0,_1T=$_.linwidth,_1R=$_.linpixs.length-1;_1T<0?_1S>=_1R:_1S<=_1R;_1S+=_1T){$_.i=_1S;$q($G($_.linpixs,$_.i,$_.linwidth));$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0}$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$q($_.linrowmult);$_.rowmult=$a();$_.pixx=$_.ccpixx+1;$k[$j++]=Infinity;var _1b=$_.linsym;var _1c=$has(_1b,"txt");$k[$j++]=_1b;$k[$j++]="txt";if(_1c){var _1d=$k[--$j];var _1e=$k[--$j];$F($g(_1e,_1d),function(){var _1g=$k[--$j];$q(_1g);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1i=$_.linsym;var _1j=$has(_1i,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1i;$k[$j++]="text1xalign";if(_1j){var _1k=$k[--$j];var _1l=$k[--$j];$k[$j++]=$g(_1l,_1k)}else{$j-=2;$k[$j++]="unset"}var _1n=$k[--$j];var _1o=$k[--$j];$_[_1o]=_1n;var _1p=$_.linsym;var _1q=$has(_1p,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1p;$k[$j++]="text1yalign";if(_1q){var _1r=$k[--$j];var _1s=$k[--$j];$k[$j++]=$g(_1s,_1r)}else{$j-=2;$k[$j++]="unset"}var _1u=$k[--$j];var _1v=$k[--$j];$_[_1v]=_1u;var _1w=$_.rowmult;var _21=0;for(var _1x=0,_1y=_1w.length;_1x<_1y;_1x++){_21=$f(_21+$g(_1w,_1x))}$_.pixy=_21;var _23=$_.linsym;var _24=$has(_23,"borderleft");$k[$j++]="borderleft";$k[$j++]=_23;$k[$j++]="borderleft";if(_24){var _25=$k[--$j];var _26=$k[--$j];$k[$j++]=$g(_26,_25)}else{$j-=2;$k[$j++]=0}var _28=$k[--$j];var _29=1-1;if(_28<1-1){var _=_29;_29=_28;_28=_}if(_28<0){_28=0}var _2B=$k[--$j];$_[_2B]=_28;var _2C=$_.linsym;var _2D=$has(_2C,"borderright");$k[$j++]="borderright";$k[$j++]=_2C;$k[$j++]="borderright";if(_2D){var _2E=$k[--$j];var _2F=$k[--$j];$k[$j++]=$g(_2F,_2E)}else{$j-=2;$k[$j++]=0}var _2H=$_.pixx;var _2I=$_.linwidth;var _2J=$k[--$j];var _2K=1;var _2L=$f(_2J-(_2H-_2I));if($f(_2J-(_2H-_2I))<1){var _=_2K;_2K=_2L;_2L=_}if(_2L<0){_2L=0}var _2N=$k[--$j];$_[_2N]=_2L;$_.bordertop=1;var _2O=$_.linsym;var _2P=$has(_2O,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2O;$k[$j++]="borderbottom";if(_2P){var _2Q=$k[--$j];var _2R=$k[--$j];$k[$j++]=$g(_2R,_2Q)}else{$j-=2;$k[$j++]=0}var _2T=$k[--$j];var _2U=$k[--$j];$_[_2U]=_2T;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarstackedomnicomposite(){if(!bwipp_databarstackedomnicomposite.globals){var $__=$_;$_=bwipp_databarstackedomnicomposite.globals={};$_.databarstackedomnicomposite_seppad=$a([0,0,0,0]);$_.databarstackedomnicomposite_findersep=$a([0,0,0,0,0,0,0,0,0,0,1,0,0]);$_.databarstackedomnicomposite_f3pat=$a([1,1,1,1,1,1,1,1,1,0,1,1,1]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarstackedomnicomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _3=$k[--$j];$_.options=_3;var _4=$k[--$j];$_.barcode=_4;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _8=$k[--$j];if(_8){var _9=$k[--$j];$_.linear=_9;$j--;var _A=$k[--$j];$_.comp=_A}else{$k[$j-1]="bwipp.missingCompositeComponent#38682";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databarstackedomni");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _H=$s($_.linear.length+$_.comp.length);$P(_H,0,$_.linear);$P(_H,$_.linear.length,$_.comp);$k[$j++]=_H;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databarstackedomni();var _N=$k[--$j];$_.bot=$G($g(_N,"pixs"),0,$g(_N,"pixx"));$_.linheight=$g(_N,"pixy");$_.linsym=_N;$_.linpixs=$g($_.linsym,"pixs");$_.linrowmult=$g($_.linsym,"rowmult");$_.sepfinder=function(){var _W=$k[--$j];$_.fp=_W;for(var _a=$_.fp,_Z=$_.fp+12;_a<=_Z;_a+=1){$_.i=_a;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _m=$k[--$j];$p($_.sep,$_.i,_m)}var _u=true;for(var _o=0;_o<=12;_o+=1){var _r=$g($_.bot,$f(_o+$_.fp));_u=_u&&$eq(_r,$g($_.databarstackedomnicomposite_f3pat,_o))}if(_u){$P($_.sep,$_.fp,$_.databarstackedomnicomposite_findersep)}};$k[$j++]=Infinity;var _x=$_.bot;for(var _y=0,_z=_x.length;_y<_z;_y++){$k[$j++]=1-$g(_x,_y)}$_.sep=$a();$P($_.sep,0,$_.databarstackedomnicomposite_seppad);$P($_.sep,$_.sep.length-4,$_.databarstackedomnicomposite_seppad);$k[$j++]=18;$_.sepfinder();$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _16=$k[--$j];$_.compsym=_16;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$_.linwidth=$_.sep.length;$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _1I=0,_1J=$_.ccpixx,_1H=$_.ccpixs.length-1;_1J<0?_1I>=_1H:_1I<=_1H;_1I+=_1J){$_.i=_1I;$k[$j++]=0;$q($G($_.ccpixs,$_.i,$_.ccpixx))}$q($_.sep);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;for(var _1S=0,_1T=$_.linwidth,_1R=$_.linpixs.length-1;_1T<0?_1S>=_1R:_1S<=_1R;_1S+=_1T){$_.i=_1S;$q($G($_.linpixs,$_.i,$_.linwidth));$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0}$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$q($_.linrowmult);$_.rowmult=$a();$_.pixx=$_.ccpixx+1;$k[$j++]=Infinity;var _1b=$_.linsym;var _1c=$has(_1b,"txt");$k[$j++]=_1b;$k[$j++]="txt";if(_1c){var _1d=$k[--$j];var _1e=$k[--$j];$F($g(_1e,_1d),function(){var _1g=$k[--$j];$q(_1g);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1i=$_.linsym;var _1j=$has(_1i,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1i;$k[$j++]="text1xalign";if(_1j){var _1k=$k[--$j];var _1l=$k[--$j];$k[$j++]=$g(_1l,_1k)}else{$j-=2;$k[$j++]="unset"}var _1n=$k[--$j];var _1o=$k[--$j];$_[_1o]=_1n;var _1p=$_.linsym;var _1q=$has(_1p,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1p;$k[$j++]="text1yalign";if(_1q){var _1r=$k[--$j];var _1s=$k[--$j];$k[$j++]=$g(_1s,_1r)}else{$j-=2;$k[$j++]="unset"}var _1u=$k[--$j];var _1v=$k[--$j];$_[_1v]=_1u;var _1w=$_.rowmult;var _21=0;for(var _1x=0,_1y=_1w.length;_1x<_1y;_1x++){_21=$f(_21+$g(_1w,_1x))}$_.pixy=_21;var _23=$_.linsym;var _24=$has(_23,"borderleft");$k[$j++]="borderleft";$k[$j++]=_23;$k[$j++]="borderleft";if(_24){var _25=$k[--$j];var _26=$k[--$j];$k[$j++]=$g(_26,_25)}else{$j-=2;$k[$j++]=0}var _28=$k[--$j];var _29=1-1;if(_28<1-1){var _=_29;_29=_28;_28=_}if(_28<0){_28=0}var _2B=$k[--$j];$_[_2B]=_28;var _2C=$_.linsym;var _2D=$has(_2C,"borderright");$k[$j++]="borderright";$k[$j++]=_2C;$k[$j++]="borderright";if(_2D){var _2E=$k[--$j];var _2F=$k[--$j];$k[$j++]=$g(_2F,_2E)}else{$j-=2;$k[$j++]=0}var _2H=$_.pixx;var _2I=$_.linwidth;var _2J=$k[--$j];var _2K=1;var _2L=$f(_2J-(_2H-_2I));if($f(_2J-(_2H-_2I))<1){var _=_2K;_2K=_2L;_2L=_}if(_2L<0){_2L=0}var _2N=$k[--$j];$_[_2N]=_2L;$_.bordertop=1;var _2O=$_.linsym;var _2P=$has(_2O,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2O;$k[$j++]="borderbottom";if(_2P){var _2Q=$k[--$j];var _2R=$k[--$j];$k[$j++]=$g(_2R,_2Q)}else{$j-=2;$k[$j++]=0}var _2T=$k[--$j];var _2U=$k[--$j];$_[_2U]=_2T;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databartruncatedcomposite(){if(!bwipp_databartruncatedcomposite.globals){var $__=$_;$_=bwipp_databartruncatedcomposite.globals={};$_.databaromnicomposite_seppadleft=$a([0,0,0]);$_.databaromnicomposite_seppadright=$a([0,0,0,0]);$_.databaromnicomposite_findersep=$a([0,0,0,0,0,0,0,0,0,0,1,0,0]);$_.databaromnicomposite_f3pat=$a([1,1,1,1,1,1,1,1,1,0,1,1,1]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databartruncatedcomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _9=$k[--$j];if(_9){var _A=$k[--$j];$_.linear=_A;$j--;var _B=$k[--$j];$_.comp=_B}else{$k[$j-1]="bwipp.missingCompositeComponent#38916";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databartruncated");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _I=$s($_.linear.length+$_.comp.length);$P(_I,0,$_.linear);$P(_I,$_.linear.length,$_.comp);$k[$j++]=_I;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databartruncated();var _O=$k[--$j];$_.linsbs=$g(_O,"sbs");$_.linheight=$g($g(_O,"bhs"),0)*72;$_.linsym=_O;$_.sepfinder=function(){var _S=$k[--$j];$_.fp=_S;for(var _W=$_.fp,_V=$_.fp+12;_W<=_V;_W+=1){$_.i=_W;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _i=$k[--$j];$p($_.sep,$_.i,_i)}var _q=true;for(var _k=0;_k<=12;_k+=1){var _n=$g($_.bot,$f(_k+$_.fp));_q=_q&&$eq(_n,$g($_.databaromnicomposite_f3pat,_k))}if(_q){$P($_.sep,$_.fp,$_.databaromnicomposite_findersep)}};$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _u=$k[--$j];var _v=$k[$j-1];var _w=_v==1?0:1;for(var _x=0,_y=_u;_x<_y;_x++){$k[$j++]=_w}});$as($a($m()-1));var _11=$k[--$j];$_.bot=_11;$j-=2;$k[$j++]=Infinity;$F($_.bot,function(){var _13=$k[--$j];$k[$j++]=1-_13});$_.sep=$a();$P($_.sep,0,$_.databaromnicomposite_seppadleft);$P($_.sep,$_.sep.length-4,$_.databaromnicomposite_seppadright);$k[$j++]=18;$_.sepfinder();$k[$j++]=64;$_.sepfinder();$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _19=$k[--$j];$_.compsym=_19;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _1H=$k[--$j];var _1I=$k[$j-1];var _1J=_1I==0?1:0;for(var _1K=0,_1L=$i(_1H);_1K<_1L;_1K++){$k[$j++]=_1J}});$_.linpixs=$a();$k[$j++]=Infinity;$k[$j++]=0;$q($_.sep);$_.sep=$a();$_.linheight=~~$_.linheight;$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _1S=0,_1T=$_.ccpixx,_1R=$_.ccpixs.length-1;_1T<0?_1S>=_1R:_1S<=_1R;_1S+=_1T){$_.i=_1S;$q($G($_.ccpixs,$_.i,$_.ccpixx));$k[$j++]=0}$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.sep);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$k[$j++]=$_.linheight;$_.rowmult=$a();$_.pixx=$_.ccpixx+1;$k[$j++]=Infinity;var _1d=$_.linsym;var _1e=$has(_1d,"txt");$k[$j++]=_1d;$k[$j++]="txt";if(_1e){var _1f=$k[--$j];var _1g=$k[--$j];$F($g(_1g,_1f),function(){var _1i=$k[--$j];$q(_1i);$r(4,-1);var _1l=$k[--$j];$k[$j++]=$f(_1l+($_.pixx-$_.linpixs.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1n=$_.linsym;var _1o=$has(_1n,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1n;$k[$j++]="text1xalign";if(_1o){var _1p=$k[--$j];var _1q=$k[--$j];$k[$j++]=$g(_1q,_1p)}else{$j-=2;$k[$j++]="unset"}var _1s=$k[--$j];var _1t=$k[--$j];$_[_1t]=_1s;var _1u=$_.linsym;var _1v=$has(_1u,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1u;$k[$j++]="text1yalign";if(_1v){var _1w=$k[--$j];var _1x=$k[--$j];$k[$j++]=$g(_1x,_1w)}else{$j-=2;$k[$j++]="unset"}var _1z=$k[--$j];var _20=$k[--$j];$_[_20]=_1z;var _21=$_.rowmult;var _26=0;for(var _22=0,_23=_21.length;_22<_23;_22++){_26=$f(_26+$g(_21,_22))}$_.pixy=_26;var _28=$_.linsym;var _29=$has(_28,"borderleft");$k[$j++]="borderleft";$k[$j++]=_28;$k[$j++]="borderleft";if(_29){var _2A=$k[--$j];var _2B=$k[--$j];$k[$j++]=$g(_2B,_2A)}else{$j-=2;$k[$j++]=10}var _2D=$_.pixx;var _2E=$_.linpixs;var _2F=$k[--$j];var _2G=1;var _2H=$f(_2F-(_2D-_2E.length+1));if($f(_2F-(_2D-_2E.length+1))<1){var _=_2G;_2G=_2H;_2H=_}if(_2H<0){_2H=0}var _2J=$k[--$j];$_[_2J]=_2H;var _2K=$_.linsym;var _2L=$has(_2K,"borderright");$k[$j++]="borderright";$k[$j++]=_2K;$k[$j++]="borderright";if(_2L){var _2M=$k[--$j];var _2N=$k[--$j];$k[$j++]=$g(_2N,_2M)}else{$j-=2;$k[$j++]=10}var _2P=$k[--$j];var _2Q=0;if(_2P<0){var _=_2Q;_2Q=_2P;_2P=_}if(_2P<0){_2P=0}var _2S=$k[--$j];$_[_2S]=_2P;$_.bordertop=1;var _2T=$_.linsym;var _2U=$has(_2T,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2T;$k[$j++]="borderbottom";if(_2U){var _2V=$k[--$j];var _2W=$k[--$j];$k[$j++]=$g(_2W,_2V)}else{$j-=2;$k[$j++]=0}var _2Y=$k[--$j];var _2Z=$k[--$j];$_[_2Z]=_2Y;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarlimitedcomposite(){if(!bwipp_databarlimitedcomposite.globals){var $__=$_;$_=bwipp_databarlimitedcomposite.globals={};$_.databarlimitedcomposite_sepleft=$a([0,0,0]);$_.databarlimitedcomposite_sepright=$a([0,0,0,0,0,0,0,0,0]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarlimitedcomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _2=$k[--$j];$_.options=_2;var _3=$k[--$j];$_.barcode=_3;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _7=$k[--$j];if(_7){var _8=$k[--$j];$_.linear=_8;$j--;var _9=$k[--$j];$_.comp=_9}else{$k[$j-1]="bwipp.missingCompositeComponent#39149";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databarlimited");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _G=$s($_.linear.length+$_.comp.length);$P(_G,0,$_.linear);$P(_G,$_.linear.length,$_.comp);$k[$j++]=_G;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databarlimited();var _M=$k[--$j];$_.linsbs=$g(_M,"sbs");$_.linheight=$g($g(_M,"bhs"),0)*72;$_.linsym=_M;$k[$j++]=Infinity;$k[$j++]=1;$F($_.linsbs,function(){var _R=$k[--$j];var _S=$k[$j-1];var _T=_S==0?1:0;for(var _U=0,_V=_R;_U<_V;_U++){$k[$j++]=_T}});$as($a($m()-1));var _Y=$k[--$j];$_.sep=_Y;$j-=2;$P($_.sep,0,$_.databarlimitedcomposite_sepleft);$P($_.sep,$_.sep.length-9,$_.databarlimitedcomposite_sepright);$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _e=$k[--$j];$_.compsym=_e;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;var _n=$G($_.linsbs,0,~~(($_.linsbs.length+1)/2)*2-1);$k[$j++]=0;for(var _o=0,_p=_n.length;_o<_p;_o++){var _r=$k[$j-1];var _s=_r==0?1:0;for(var _t=0,_u=$i($g(_n,_o));_t<_u;_t++){$k[$j++]=_s}}$_.linpixs=$a();$k[$j++]=Infinity;$k[$j++]=0;$q($G($_.sep,0,$_.linpixs.length-1));$_.sep=$a();$_.linheight=~~$_.linheight;$_.ccrows=$_.ccrowmult.length;if($_.ccpixx==72){$k[$j++]=Infinity;for(var _14=0,_15=$_.ccpixx,_13=$_.ccpixs.length-1;_15<0?_14>=_13:_14<=_13;_14+=_15){$_.i=_14;$k[$j++]=0;$q($G($_.ccpixs,$_.i,$_.ccpixx));$k[$j++]=0}$q($_.sep);$q($_.linpixs);$_.pixs=$a();$_.pixx=$_.linpixs.length}else{$k[$j++]=Infinity;for(var _1G=0,_1H=$_.ccpixx,_1F=$_.ccpixs.length-1;_1H<0?_1G>=_1F:_1G<=_1F;_1G+=_1H){$_.i=_1G;$q($G($_.ccpixs,$_.i,$_.ccpixx));$k[$j++]=0}$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.sep);$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$k[$j++]=0;$q($_.linpixs);$_.pixs=$a();$_.pixx=$_.ccpixx+1}$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$k[$j++]=$_.linheight;$_.rowmult=$a();$k[$j++]=Infinity;var _1R=$_.linsym;var _1S=$has(_1R,"txt");$k[$j++]=_1R;$k[$j++]="txt";if(_1S){var _1T=$k[--$j];var _1U=$k[--$j];$F($g(_1U,_1T),function(){var _1W=$k[--$j];$q(_1W);$r(4,-1);var _1Z=$k[--$j];$k[$j++]=$f(_1Z+($_.pixx-$_.linpixs.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1b=$_.linsym;var _1c=$has(_1b,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1b;$k[$j++]="text1xalign";if(_1c){var _1d=$k[--$j];var _1e=$k[--$j];$k[$j++]=$g(_1e,_1d)}else{$j-=2;$k[$j++]="unset"}var _1g=$k[--$j];var _1h=$k[--$j];$_[_1h]=_1g;var _1i=$_.linsym;var _1j=$has(_1i,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1i;$k[$j++]="text1yalign";if(_1j){var _1k=$k[--$j];var _1l=$k[--$j];$k[$j++]=$g(_1l,_1k)}else{$j-=2;$k[$j++]="unset"}var _1n=$k[--$j];var _1o=$k[--$j];$_[_1o]=_1n;var _1p=$_.rowmult;var _1u=0;for(var _1q=0,_1r=_1p.length;_1q<_1r;_1q++){_1u=$f(_1u+$g(_1p,_1q))}$_.pixy=_1u;var _1w=$_.linsym;var _1x=$has(_1w,"borderleft");$k[$j++]="borderleft";$k[$j++]=_1w;$k[$j++]="borderleft";if(_1x){var _1y=$k[--$j];var _1z=$k[--$j];$k[$j++]=$g(_1z,_1y)}else{$j-=2;$k[$j++]=10}var _21=$_.pixx;var _22=$_.linpixs;var _23=$k[--$j];var _24=$_.pixx;var _25=$_.ccpixx;var _26=1-($f(_24-_25)-1);var _27=$f(_23-(_21-_22.length+1));if($f(_23-(_21-_22.length+1))<1-($f(_24-_25)-1)){var _=_26;_26=_27;_27=_}if(_27<0){_27=0}var _29=$k[--$j];$_[_29]=_27;var _2A=$_.linsym;var _2B=$has(_2A,"borderright");$k[$j++]="borderright";$k[$j++]=_2A;$k[$j++]="borderright";if(_2B){var _2C=$k[--$j];var _2D=$k[--$j];$k[$j++]=$g(_2D,_2C)}else{$j-=2;$k[$j++]=10}var _2F=$k[--$j];var _2G=0;if(_2F<0){var _=_2G;_2G=_2F;_2F=_}if(_2F<0){_2F=0}var _2I=$k[--$j];$_[_2I]=_2F;$_.bordertop=1;var _2J=$_.linsym;var _2K=$has(_2J,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2J;$k[$j++]="borderbottom";if(_2K){var _2L=$k[--$j];var _2M=$k[--$j];$k[$j++]=$g(_2M,_2L)}else{$j-=2;$k[$j++]=0}var _2O=$k[--$j];var _2P=$k[--$j];$_[_2P]=_2O;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarexpandedcomposite(){var $__=$_;$_=Object.assign({},$_,bwipp_databarexpandedcomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#39368";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databarexpanded");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _E=$s($_.linear.length+$_.comp.length);$P(_E,0,$_.linear);$P(_E,$_.linear.length,$_.comp);$k[$j++]=_E;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databarexpanded();var _K=$k[--$j];$_.linsbs=$g(_K,"sbs");$_.linheight=$g($g(_K,"bhs"),0)*72;$_.linsym=_K;$_.sepfinder=function(){var _O=$k[--$j];$_.fp=_O;for(var _S=$_.fp,_R=$_.fp+12;_S<=_R;_S+=1){$_.i=_S;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _e=$k[--$j];$p($_.sep,$_.i,_e)}};$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _h=$k[--$j];var _i=$k[$j-1];var _j=_i==1?0:1;for(var _k=0,_l=_h;_k<_l;_k++){$k[$j++]=_j}});$as($a($m()-1));var _o=$k[--$j];$_.bot=_o;$j-=2;$k[$j++]=Infinity;$F($_.bot,function(){var _q=$k[--$j];$k[$j++]=1-_q});$_.sep=$a();$P($_.sep,0,$a([0,0,0]));$P($_.sep,$_.sep.length-4,$a([0,0,0,0]));$k[$j++]=Infinity;for(var _y=18,_x=$_.bot.length-13;_y<=_x;_y+=98){$k[$j++]=_y}for(var _11=69,_10=$_.bot.length-13;_11<=_10;_11+=98){$k[$j++]=_11}var _12=$a();for(var _13=0,_14=_12.length;_13<_14;_13++){$k[$j++]=$g(_12,_13);$_.sepfinder()}$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _18=$k[--$j];$_.compsym=_18;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;$k[$j++]=0;$F($_.linsbs,function(){var _1G=$k[--$j];var _1H=$k[$j-1];var _1I=_1H==0?1:0;for(var _1J=0,_1K=$i(_1G);_1J<_1K;_1J++){$k[$j++]=_1I}});$_.linpixs=$a();$k[$j++]=Infinity;$k[$j++]=0;$q($_.sep);$_.sep=$a();$_.linheight=~~$_.linheight;$_.ccrows=$_.ccrowmult.length;$_.diff=$f($_.linpixs.length-$_.ccpixx);$k[$j++]=Infinity;for(var _1R=0,_1S=$_.diff-2;_1R<_1S;_1R++){$k[$j++]=0}$_.ccrpad=$a();$k[$j++]=Infinity;for(var _1W=0,_1X=$_.ccpixx,_1V=$_.ccpixs.length-1;_1X<0?_1W>=_1V:_1W<=_1V;_1W+=_1X){$_.i=_1W;$k[$j++]=0;$k[$j++]=0;$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.sep);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$k[$j++]=$_.linheight;$_.rowmult=$a();$_.pixx=$_.linpixs.length;$k[$j++]=Infinity;var _1i=$_.linsym;var _1j=$has(_1i,"txt");$k[$j++]=_1i;$k[$j++]="txt";if(_1j){var _1k=$k[--$j];var _1l=$k[--$j];$F($g(_1l,_1k),function(){var _1n=$k[--$j];$q(_1n);$r(4,-1);var _1q=$k[--$j];$k[$j++]=$f(_1q+($_.pixx-$_.linpixs.length+1));$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1s=$_.linsym;var _1t=$has(_1s,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1s;$k[$j++]="text1xalign";if(_1t){var _1u=$k[--$j];var _1v=$k[--$j];$k[$j++]=$g(_1v,_1u)}else{$j-=2;$k[$j++]="unset"}var _1x=$k[--$j];var _1y=$k[--$j];$_[_1y]=_1x;var _1z=$_.linsym;var _20=$has(_1z,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1z;$k[$j++]="text1yalign";if(_20){var _21=$k[--$j];var _22=$k[--$j];$k[$j++]=$g(_22,_21)}else{$j-=2;$k[$j++]="unset"}var _24=$k[--$j];var _25=$k[--$j];$_[_25]=_24;var _26=$_.rowmult;var _2B=0;for(var _27=0,_28=_26.length;_27<_28;_27++){_2B=$f(_2B+$g(_26,_27))}$_.pixy=_2B;var _2D=$_.linsym;var _2E=$has(_2D,"borderleft");$k[$j++]="borderleft";$k[$j++]=_2D;$k[$j++]="borderleft";if(_2E){var _2F=$k[--$j];var _2G=$k[--$j];$k[$j++]=$g(_2G,_2F)}else{$j-=2;$k[$j++]=10}var _2I=$_.pixx;var _2J=$_.linpixs;var _2K=$k[--$j];var _2L=1-2;var _2M=$f(_2K-(_2I-_2J.length+1));if($f(_2K-(_2I-_2J.length+1))<1-2){var _=_2L;_2L=_2M;_2M=_}if(_2M<0){_2M=0}var _2O=$k[--$j];$_[_2O]=_2M;var _2P=$_.linsym;var _2Q=$has(_2P,"borderright");$k[$j++]="borderright";$k[$j++]=_2P;$k[$j++]="borderright";if(_2Q){var _2R=$k[--$j];var _2S=$k[--$j];$k[$j++]=$g(_2S,_2R)}else{$j-=2;$k[$j++]=10}var _2U=$_.ccrpad;var _2V=$k[--$j];var _2W=1-_2U.length;if(_2V<1-_2U.length){var _=_2W;_2W=_2V;_2V=_}if(_2V<0){_2V=0}var _2Y=$k[--$j];$_[_2Y]=_2V;$_.bordertop=1;var _2Z=$_.linsym;var _2a=$has(_2Z,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2Z;$k[$j++]="borderbottom";if(_2a){var _2b=$k[--$j];var _2c=$k[--$j];$k[$j++]=$g(_2c,_2b)}else{$j-=2;$k[$j++]=0}var _2e=$k[--$j];var _2f=$k[--$j];$_[_2f]=_2e;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_databarexpandedstackedcomposite(){if(!bwipp_databarexpandedstackedcomposite.globals){var $__=$_;$_=bwipp_databarexpandedstackedcomposite.globals={};$_.databarexpandedstackedcomposite_seppad=$a([0,0,0,0]);$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_databarexpandedstackedcomposite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _1=$k[--$j];$_.options=_1;var _2=$k[--$j];$_.barcode=_2;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _6=$k[--$j];if(_6){var _7=$k[--$j];$_.linear=_7;$j--;var _8=$k[--$j];$_.comp=_8}else{$k[$j-1]="bwipp.missingCompositeComponent#39601";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"lintype","databarexpandedstacked");$p($_.options,"linkage",true);$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _F=$s($_.linear.length+$_.comp.length);$P(_F,0,$_.linear);$P(_F,$_.linear.length,$_.comp);$k[$j++]=_F;$k[$j++]="ai";bwipp_gs1process();$j-=3;$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=$_.options;bwipp_databarexpandedstacked();var _L=$k[--$j];$_.bot=$G($g(_L,"pixs"),0,$g(_L,"pixx"));$_.linheight=$g(_L,"pixy");$_.linsym=_L;$_.linpixs=$g($_.linsym,"pixs");$_.linrowmult=$g($_.linsym,"rowmult");$_.sepfinder=function(){var _U=$k[--$j];$_.fp=_U;for(var _Y=$_.fp,_X=$_.fp+12;_Y<=_X;_Y+=1){$_.i=_Y;if($g($_.bot,$_.i)==0){if($g($_.bot,$_.i-1)==1){$k[$j++]=1}else{$k[$j++]=$g($_.sep,$_.i-1)==0?1:0}}else{$k[$j++]=0}var _k=$k[--$j];$p($_.sep,$_.i,_k)}};$k[$j++]=Infinity;var _m=$_.bot;for(var _n=0,_o=_m.length;_n<_o;_n++){$k[$j++]=1-$g(_m,_n)}$_.sep=$a();$P($_.sep,0,$_.databarexpandedstackedcomposite_seppad);$P($_.sep,$_.sep.length-4,$_.databarexpandedstackedcomposite_seppad);$k[$j++]=Infinity;for(var _v=19,_u=$_.bot.length-13;_v<=_u;_v+=98){$k[$j++]=_v}for(var _y=70,_x=$_.bot.length-13;_y<=_x;_y+=98){$k[$j++]=_y}var _z=$a();for(var _10=0,_11=_z.length;_10<_11;_10++){$k[$j++]=$g(_z,_10);$_.sepfinder()}$k[$j++]=$_.comp;$k[$j++]=$_.options;bwipp_gs1_cc();var _15=$k[--$j];$_.compsym=_15;$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$_.pixx=$_.sep.length;$k[$j++]=Infinity;for(var _1F=0,_1G=~~(($f($_.pixx-$_.ccpixx)+1)/2);_1F<_1G;_1F++){$k[$j++]=0}$_.cclpad=$a();$k[$j++]=Infinity;for(var _1J=0,_1K=~~($f($_.pixx-$_.ccpixx)/2);_1J<_1K;_1J++){$k[$j++]=0}$_.ccrpad=$a();$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _1P=0,_1Q=$_.ccpixx,_1O=$_.ccpixs.length-1;_1Q<0?_1P>=_1O:_1P<=_1O;_1P+=_1Q){$_.i=_1P;$q($_.cclpad);$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.sep);$q($_.linpixs);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$q($_.linrowmult);$_.rowmult=$a();$k[$j++]=Infinity;var _1b=$_.linsym;var _1c=$has(_1b,"txt");$k[$j++]=_1b;$k[$j++]="txt";if(_1c){var _1d=$k[--$j];var _1e=$k[--$j];$F($g(_1e,_1d),function(){var _1g=$k[--$j];$q(_1g);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1i=$_.linsym;var _1j=$has(_1i,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1i;$k[$j++]="text1xalign";if(_1j){var _1k=$k[--$j];var _1l=$k[--$j];$k[$j++]=$g(_1l,_1k)}else{$j-=2;$k[$j++]="unset"}var _1n=$k[--$j];var _1o=$k[--$j];$_[_1o]=_1n;var _1p=$_.linsym;var _1q=$has(_1p,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1p;$k[$j++]="text1yalign";if(_1q){var _1r=$k[--$j];var _1s=$k[--$j];$k[$j++]=$g(_1s,_1r)}else{$j-=2;$k[$j++]="unset"}var _1u=$k[--$j];var _1v=$k[--$j];$_[_1v]=_1u;var _1w=$_.rowmult;var _21=0;for(var _1x=0,_1y=_1w.length;_1x<_1y;_1x++){_21=$f(_21+$g(_1w,_1x))}$_.pixy=_21;var _23=$_.linsym;var _24=$has(_23,"borderleft");$k[$j++]="borderleft";$k[$j++]=_23;$k[$j++]="borderleft";if(_24){var _25=$k[--$j];var _26=$k[--$j];$k[$j++]=$g(_26,_25)}else{$j-=2;$k[$j++]=0}var _28=$_.cclpad;var _29=$k[--$j];var _2A=1-_28.length;if(_29<1-_28.length){var _=_2A;_2A=_29;_29=_}if(_29<0){_29=0}var _2C=$k[--$j];$_[_2C]=_29;var _2D=$_.linsym;var _2E=$has(_2D,"borderright");$k[$j++]="borderright";$k[$j++]=_2D;$k[$j++]="borderright";if(_2E){var _2F=$k[--$j];var _2G=$k[--$j];$k[$j++]=$g(_2G,_2F)}else{$j-=2;$k[$j++]=0}var _2I=$_.ccrpad;var _2J=$k[--$j];var _2K=1-_2I.length;if(_2J<1-_2I.length){var _=_2K;_2K=_2J;_2J=_}if(_2J<0){_2J=0}var _2M=$k[--$j];$_[_2M]=_2J;$_.bordertop=1;var _2N=$_.linsym;var _2O=$has(_2N,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2N;$k[$j++]="borderbottom";if(_2O){var _2P=$k[--$j];var _2Q=$k[--$j];$k[$j++]=$g(_2Q,_2P)}else{$j-=2;$k[$j++]=0}var _2S=$k[--$j];var _2T=$k[--$j];$_[_2T]=_2S;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1_128composite(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1_128composite.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.width=0;$_.modunit=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$X($_.barcode,"|");var _5=$k[--$j];if(_5){var _6=$k[--$j];$_.linear=_6;$j--;var _7=$k[--$j];$_.comp=_7}else{$k[$j-1]="bwipp.missingCompositeComponent#39824";$k[$j++]="A Composite Component must be provided following a pipe character";bwipp_raiseerror()}$p($_.options,"inkspreadv",0);$p($_.options,"dontlint",true);var _C=$s($_.linear.length+$_.comp.length);$P(_C,0,$_.linear);$P(_C,$_.linear.length,$_.comp);$k[$j++]=_C;$k[$j++]="ai";bwipp_gs1process();$j-=3;$p($_.options,"linkagea",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.linear;$k[$j++]=Infinity;$F($_.options);var _J=$d();$k[$j++]=_J;bwipp_gs1_128();var _K=$k[--$j];var _O=0;$F($g(_K,"sbs"),function(){var _M=$k[--$j];_O=$f(_O+_M)});$_.linwidth=_O;$p($_.options,"lintype","gs1-128");$p($_.options,"linwidth",$_.linwidth);$k[$j++]=$_.comp;$k[$j++]=Infinity;$F($_.options);var _U=$d();$k[$j++]=_U;bwipp_gs1_cc();var _V=$k[--$j];$_.compsym=_V;var _Y=$g($_.compsym,"pixx")==99?"a":"c";$_.linktype=_Y;$p($_.compsym,"borderbottom",0);if($eq($_.linktype,"a")){$p($_.options,"linkagea",true);$p($_.options,"linkagec",false)}else{$p($_.options,"linkagea",false);$p($_.options,"linkagec",true)}$k[$j++]=$_.linear;$k[$j++]=Infinity;$F($_.options);var _h=$d();$k[$j++]=_h;bwipp_gs1_128();var _i=$k[--$j];$_.linsbs=$g(_i,"sbs");$_.linheight=$g($g(_i,"bhs"),0)*72;$_.linsym=_i;$k[$j++]=Infinity;$k[$j++]=1;$F($_.linsbs,function(){var _n=$k[--$j];var _o=$k[$j-1];var _p=_o==0?1:0;for(var _q=0,_r=_n;_q<_r;_q++){$k[$j++]=_p}});$as($a($m()-1));var _u=$k[--$j];$_.sep=_u;$j-=2;if($eq($_.linktype,"a")){$_.s=~~(($_.linwidth-2)/11);$_.p=~~(($_.s-9)/2);var _12=($_.s-$_.p-1)*11+10;if($_.p==0){_12=_12+2}$_.x=_12-99}else{$_.x=-7}$_.ccpixs=$g($_.compsym,"pixs");$_.ccrowmult=$g($_.compsym,"rowmult");$_.ccpixx=$g($_.compsym,"pixx");$k[$j++]=Infinity;$F($_.linsbs,function(){var _1B=$k[--$j];var _1C=$k[$j-1];var _1D=_1C==1?0:1;for(var _1E=0,_1F=_1B;_1E<_1F;_1E++){$k[$j++]=_1D}});$_.linpixs=$a();if($_.x>0){$k[$j++]=Infinity;for(var _1I=0,_1J=$_.x;_1I<_1J;_1I++){$k[$j++]=0}$_.cclpad=$a();$_.linlpad=$a(0)}else{$_.cclpad=$a(0);$k[$j++]=Infinity;for(var _1N=0,_1O=-$_.x;_1N<_1O;_1N++){$k[$j++]=0}$_.linlpad=$a()}$_.diff=$f($_.linwidth-($_.ccpixx+$_.x));if($_.diff>0){$k[$j++]=Infinity;for(var _1U=0,_1V=$_.diff;_1U<_1V;_1U++){$k[$j++]=0}$_.ccrpad=$a();$_.linrpad=$a(0)}else{$_.ccrpad=$a(0);$k[$j++]=Infinity;for(var _1Z=0,_1a=-$_.diff;_1Z<_1a;_1Z++){$k[$j++]=0}$_.linrpad=$a()}$k[$j++]=Infinity;var _1b=$_.linsym;var _1c=$has(_1b,"txt");$k[$j++]=_1b;$k[$j++]="txt";if(_1c){var _1d=$k[--$j];var _1e=$k[--$j];$F($g(_1e,_1d),function(){var _1g=$k[--$j];$q(_1g);$r(4,-1);var _1i=$k[--$j];$k[$j++]=$f(_1i+$_.linlpad.length);$r(4,1);$as($a(5))})}else{$j-=2}$_.txt=$a();var _1k=$_.linsym;var _1l=$has(_1k,"text1xalign");$k[$j++]="text1xalign";$k[$j++]=_1k;$k[$j++]="text1xalign";if(_1l){var _1m=$k[--$j];var _1n=$k[--$j];$k[$j++]=$g(_1n,_1m)}else{$j-=2;$k[$j++]="unset"}var _1p=$k[--$j];var _1q=$k[--$j];$_[_1q]=_1p;var _1r=$_.linsym;var _1s=$has(_1r,"text1yalign");$k[$j++]="text1yalign";$k[$j++]=_1r;$k[$j++]="text1yalign";if(_1s){var _1t=$k[--$j];var _1u=$k[--$j];$k[$j++]=$g(_1u,_1t)}else{$j-=2;$k[$j++]="unset"}var _1w=$k[--$j];var _1x=$k[--$j];$_[_1x]=_1w;$_.linheight=~~$_.linheight;$_.ccrows=$_.ccrowmult.length;$k[$j++]=Infinity;for(var _23=0,_24=$_.ccpixx,_22=$_.ccpixs.length-1;_24<0?_23>=_22:_23<=_22;_23+=_24){$_.i=_23;$q($_.cclpad);$q($G($_.ccpixs,$_.i,$_.ccpixx));$q($_.ccrpad)}$q($_.linlpad);$q($_.sep);$q($_.linrpad);$q($_.linlpad);$q($_.linpixs);$q($_.linrpad);$_.pixs=$a();$k[$j++]=Infinity;$q($_.ccrowmult);$k[$j++]=1;$k[$j++]=$_.linheight;$_.rowmult=$a();$_.pixx=$f($_.cclpad.length+$_.ccpixx+$_.ccrpad.length);var _2M=$_.rowmult;var _2R=0;for(var _2N=0,_2O=_2M.length;_2N<_2O;_2N++){_2R=$f(_2R+$g(_2M,_2N))}$_.pixy=_2R;var _2U=$eq($_.linktype,"a")?1:2;$_.ccqz=_2U;var _2V=$_.linsym;var _2W=$has(_2V,"borderleft");$k[$j++]="borderleft";$k[$j++]=_2V;$k[$j++]="borderleft";if(_2W){var _2X=$k[--$j];var _2Y=$k[--$j];$k[$j++]=$g(_2Y,_2X)}else{$j-=2;$k[$j++]=10}var _2a=$_.linlpad;var _2b=$k[--$j];var _2c=$_.ccqz;var _2d=$_.cclpad;var _2e=_2c-_2d.length;var _2f=$f(_2b-_2a.length);if($f(_2b-_2a.length)<_2c-_2d.length){var _=_2e;_2e=_2f;_2f=_}if(_2f<0){_2f=0}var _2h=$k[--$j];$_[_2h]=_2f;var _2i=$_.linsym;var _2j=$has(_2i,"borderright");$k[$j++]="borderright";$k[$j++]=_2i;$k[$j++]="borderright";if(_2j){var _2k=$k[--$j];var _2l=$k[--$j];$k[$j++]=$g(_2l,_2k)}else{$j-=2;$k[$j++]=10}var _2n=$_.linrpad;var _2o=$k[--$j];var _2p=$_.ccqz;var _2q=$_.ccrpad;var _2r=_2p-_2q.length;var _2s=$f(_2o-_2n.length);if($f(_2o-_2n.length)<_2p-_2q.length){var _=_2r;_2r=_2s;_2s=_}if(_2s<0){_2s=0}var _2u=$k[--$j];$_[_2u]=_2s;$_.bordertop=$_.ccqz;var _2w=$_.linsym;var _2x=$has(_2w,"borderbottom");$k[$j++]="borderbottom";$k[$j++]=_2w;$k[$j++]="borderbottom";if(_2x){var _2y=$k[--$j];var _2z=$k[--$j];$k[$j++]=$g(_2z,_2y)}else{$j-=2;$k[$j++]=0}var _31=$k[--$j];var _32=$k[--$j];$_[_32]=_31;$del($_.options,"height");$k[$j++]=new Map([["ren","renmatrix"],["pixs",$_.pixs],["pixx",$_.pixx],["pixy",$_.pixy],["rowmult",$_.rowmult],["height",$_.pixy/72],["width",$_.pixx/72],["borderleft",$_.borderleft],["borderright",$_.borderright],["bordertop",$_.bordertop],["borderbottom",$_.borderbottom],["txt",$_.txt],["text1xalign",$_.text1xalign],["text1yalign",$_.text1yalign],["opt",$_.options]]);if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1datamatrix(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1datamatrix.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.gssep=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;if($_.barcode.length>4e3){$k[$j++]="bwipp.gs1datamatrixinputTooLong#40056";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _6=$k[--$j];$_.fncs=_6;var _7=$k[--$j];$_.vals=_7;var _8=$k[--$j];$_.ais=_8;$_.fnc1=-1;$k[$j++]=Infinity;$k[$j++]=$_.fnc1;for(var _C=0,_B=$_.ais.length-1;_C<=_B;_C+=1){$_.i=_C;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){if($_.gssep){$k[$j++]=29}else{$k[$j++]=$_.fnc1}}}$as($a($m()));var _S=$k[--$j];$_.dmtx=_S;$j--;$_.barcode=$s(($_.dmtx.length+1)*5);$_.j=0;$F($_.dmtx,function(){var _W=$k[$j-1];if(_W==$_.fnc1){$j--;$P($_.barcode,$_.j,"^FNC1");$_.j=$_.j+5}else{var _c=$k[--$j];$p($_.barcode,$_.j,_c);$_.j=$_.j+1}});$_.barcode=$G($_.barcode,0,$_.j);$del($_.options,"parse");$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _m=$k[--$j];$_.args=_m;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1datamatrixrectangular(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1datamatrixrectangular.globals);$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.dontdraw=false;$_.gssep=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;if($_.barcode.length>4e3){$k[$j++]="bwipp.gs1datamatrixrectangularinputTooLong#40188";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _6=$k[--$j];$_.fncs=_6;var _7=$k[--$j];$_.vals=_7;var _8=$k[--$j];$_.ais=_8;$_.fnc1=-1;$k[$j++]=Infinity;$k[$j++]=$_.fnc1;for(var _C=0,_B=$_.ais.length-1;_C<=_B;_C+=1){$_.i=_C;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){if($_.gssep){$k[$j++]=29}else{$k[$j++]=$_.fnc1}}}$as($a($m()));var _S=$k[--$j];$_.dmtx=_S;$j--;$_.barcode=$s(($_.dmtx.length+1)*5);$_.j=0;$F($_.dmtx,function(){var _W=$k[$j-1];if(_W==$_.fnc1){$j--;$P($_.barcode,$_.j,"^FNC1");$_.j=$_.j+5}else{var _c=$k[--$j];$p($_.barcode,$_.j,_c);$_.j=$_.j+1}});$_.barcode=$G($_.barcode,0,$_.j);$del($_.options,"parse");$p($_.options,"parsefnc",true);$p($_.options,"format","rectangle");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _n=$k[--$j];$_.args=_n;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1dldatamatrix(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1dldatamatrix.globals);$_.includetext=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _7=$k[--$j];$_.barcode=_7;$k[$j++]=$_.barcode;$k[$j++]="dl";bwipp_gs1process();$j--;var _9=$k[--$j];$_.val=$g(_9,0);var _B=$k[--$j];$_.key=$g(_B,0);$_.hri=$s($_.key.length+$_.val.length+2);$p($_.hri,0,40);$P($_.hri,1,$_.key);$p($_.hri,$_.key.length+1,41);$P($_.hri,$_.key.length+2,$_.val);$del($_.options,"parse");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _R=$k[--$j];$_.args=_R;$p($_.args,"opt",$_.options);if($_.includetext){$p($_.args,"text1",$_.hri);$p($_.args,"text1yoffset",-1);$p($_.args,"text1font","OCR-B");$p($_.args,"text1size",5)}$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1qrcode(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1qrcode.globals);$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;if($_.barcode.length>4e3){$k[$j++]="bwipp.gs1qrcodeinputTooLong#40438";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _6=$k[--$j];$_.fncs=_6;var _7=$k[--$j];$_.vals=_7;var _8=$k[--$j];$_.ais=_8;$_.fnc1=-1;$k[$j++]=Infinity;$k[$j++]=$_.fnc1;for(var _C=0,_B=$_.ais.length-1;_C<=_B;_C+=1){$_.i=_C;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){$k[$j++]=$_.fnc1}}$as($a($m()));var _R=$k[--$j];$_.qrc=_R;$j--;$_.barcode=$s(($_.qrc.length+1)*5);$_.j=0;$F($_.qrc,function(){var _V=$k[$j-1];if(_V==$_.fnc1){$j--;$P($_.barcode,$_.j,"^FNC1");$_.j=$_.j+5}else{var _b=$k[--$j];$p($_.barcode,$_.j,_b);$_.j=$_.j+1}});$_.barcode=$G($_.barcode,0,$_.j);$del($_.options,"parse");$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _l=$k[--$j];$_.args=_l;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1dlqrcode(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1dlqrcode.globals);$_.includetext=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;$_.dontdraw=false;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$_.fncvals=new Map([["parse",$_.parse],["parseonly",true],["parsefnc",false]]);$k[$j++]=$_.barcode;$k[$j++]=$_.fncvals;bwipp_parseinput();var _7=$k[--$j];$_.barcode=_7;$k[$j++]=$_.barcode;$k[$j++]="dl";bwipp_gs1process();$j--;var _9=$k[--$j];$_.val=$g(_9,0);var _B=$k[--$j];$_.key=$g(_B,0);$_.hri=$s($_.key.length+$_.val.length+2);$p($_.hri,0,40);$P($_.hri,1,$_.key);$p($_.hri,$_.key.length+1,41);$P($_.hri,$_.key.length+2,$_.val);$del($_.options,"parse");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _R=$k[--$j];$_.args=_R;$p($_.args,"opt",$_.options);if($_.includetext){$p($_.args,"text1",$_.hri);$p($_.args,"text1yoffset",-4);$p($_.args,"text1font","OCR-B");$p($_.args,"text1size",5)}$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_gs1dotcode(){var $__=$_;$_=Object.assign({},$_,bwipp_gs1dotcode.globals);$_.dontdraw=false;$_.parse=false;$_.dontlint=false;$_.lintreqs=true;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;if($_.barcode.length>4e3){$k[$j++]="bwipp.gs1dotcodeinputTooLong#40687";$k[$j++]="The input data is too long";bwipp_raiseerror()}$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));$k[$j++]=$_.barcode;$k[$j++]="ai";bwipp_gs1process();var _6=$k[--$j];$_.fncs=_6;var _7=$k[--$j];$_.vals=_7;var _8=$k[--$j];$_.ais=_8;$_.fnc1=-1;$k[$j++]=Infinity;$k[$j++]=$_.fnc1;for(var _C=0,_B=$_.ais.length-1;_C<=_B;_C+=1){$_.i=_C;$F($g($_.ais,$_.i));$F($g($_.vals,$_.i));if($_.i!=$_.ais.length-1&&$g($_.fncs,$_.i)){$k[$j++]=$_.fnc1}}$as($a($m()));var _R=$k[--$j];$_.dcode=_R;$j--;$_.barcode=$s(($_.dcode.length+1)*5);$_.j=0;$F($_.dcode,function(){var _V=$k[$j-1];if(_V==$_.fnc1){$j--;$P($_.barcode,$_.j,"^FNC1");$_.j=$_.j+5}else{var _b=$k[--$j];$p($_.barcode,$_.j,_b);$_.j=$_.j+1}});$_.barcode=$G($_.barcode,0,$_.j);$del($_.options,"parse");$p($_.options,"parsefnc",true);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_dotcode();var _l=$k[--$j];$_.args=_l;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibccode39(){if(!bwipp_hibccode39.globals){var $__=$_;$_=bwipp_hibccode39.globals={};$_.hibccode39_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibccode39_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibccode39.globals);$_.dontdraw=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.text1xoffset=0;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibccode39emptyData#40847";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.hibccode39inputTooLong#40851";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibccode39badCharacter#40859";$k[$j++]="HIBC Code 39 must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibccode39_barchars,$_.checksum)){$k[$j++]="bwipp.hibccode39badCheckDigit#40873";$k[$j++]="Incorrect HIBC Code 39 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibccode39_barchars,$_.checksum));$_.text=$s($_.barlen+4);$p($_.text,0,42);$P($_.text,1,$_.barcode);var _v=$g($_.barcode,$_.barlen+1);var _y=$_.text;var _x=$_.barlen+2;if(_v==32){_v=95}$p(_y,_x,_v);$p($_.text,$_.barlen+3,42);$p($_.options,"includecheck",false);$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code39();var _15=$k[--$j];$_.args=_15;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_hibccode128(){if(!bwipp_hibccode128.globals){var $__=$_;$_=bwipp_hibccode128.globals={};$_.hibccode128_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibccode128_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibccode128.globals);$_.dontdraw=false;$_.text1font="OCR-B";$_.text1size=10;$_.textyoffset=-8;$_.text1xoffset=0;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibccode128emptyData#40989";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>500){$k[$j++]="bwipp.hibccode128inputTooLong#40993";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibccode128badCharacter#41001";$k[$j++]="HIBC Code 128 must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibccode128_barchars,$_.checksum)){$k[$j++]="bwipp.hibccode128badCheckDigit#41015";$k[$j++]="Incorrect HIBC Code 128 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibccode128_barchars,$_.checksum));$_.text=$s($_.barlen+4);$p($_.text,0,42);$P($_.text,1,$_.barcode);var _v=$g($_.barcode,$_.barlen+1);var _y=$_.text;var _x=$_.barlen+2;if(_v==32){_v=95}$p(_y,_x,_v);$p($_.text,$_.barlen+3,42);$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_code128();var _14=$k[--$j];$_.args=_14;$p($_.args,"txt",$a([$a([$_.text,$_.text1xoffset,$_.text1yoffset,$_.text1font,$_.text1size])]));$p($_.args,"text1xalign","center");$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_hibcdatamatrix(){if(!bwipp_hibcdatamatrix.globals){var $__=$_;$_=bwipp_hibcdatamatrix.globals={};$_.hibcdatamatrix_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcdatamatrix_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcdatamatrix.globals);$_.dontdraw=false;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcdatamatrixEmptyData#41129";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>15e3){$k[$j++]="bwipp.hibcdatamatrixinputTooLong#41133";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcdatamatrixBadCharacter#41138";$k[$j++]="HIBC Data Matrix must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcdatamatrix_barchars,$_.checksum)){$k[$j++]="bwipp.hibcdatamatrixBadCheckDigit#41152";$k[$j++]="Incorrect HIBC Data Matrix check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcdatamatrix_barchars,$_.checksum));$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _p=$k[--$j];$_.args=_p;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibcdatamatrixrectangular(){if(!bwipp_hibcdatamatrixrectangular.globals){var $__=$_;$_=bwipp_hibcdatamatrixrectangular.globals={};$_.hibcdatamatrixrectangular_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcdatamatrixrectangular_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcdatamatrixrectangular.globals);$_.dontdraw=false;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcdatamatrixrectangularEmptyData#41258";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>15e3){$k[$j++]="bwipp.hibcdatamatrixrectangularinputTooLong#41262";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcdatamatrixrectangularBadCharacter#41267";$k[$j++]="HIBC Data Matrix Rectangular must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcdatamatrixrectangular_barchars,$_.checksum)){$k[$j++]="bwipp.hibcdatamatrixrectangularBadCheckDigit#41281";$k[$j++]="Incorrect HIBC Data Matrix Rectangular check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcdatamatrixrectangular_barchars,$_.checksum));$p($_.options,"validatecheck",false);$p($_.options,"format","rectangle");$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_datamatrix();var _q=$k[--$j];$_.args=_q;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibcpdf417(){if(!bwipp_hibcpdf417.globals){var $__=$_;$_=bwipp_hibcpdf417.globals={};$_.hibcpdf417_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcpdf417_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcpdf417.globals);$_.dontdraw=false;$_.columns=2;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcpdf417EmptyData#41389";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>12500){$k[$j++]="bwipp.hibcpdf417inputTooLong#41393";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcpdf417BadCharacter#41398";$k[$j++]="HIBC PDF417 must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcpdf417_barchars,$_.checksum)){$k[$j++]="bwipp.hibcpdf417BadCheckDigit#41412";$k[$j++]="Incorrect HIBC PDF417 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcpdf417_barchars,$_.checksum));$p($_.options,"columns",$_.columns);$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_pdf417();var _r=$k[--$j];$_.args=_r;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibcmicropdf417(){if(!bwipp_hibcmicropdf417.globals){var $__=$_;$_=bwipp_hibcmicropdf417.globals={};$_.hibcmicropdf417_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcmicropdf417_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcmicropdf417.globals);$_.dontdraw=false;$_.columns=2;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcmicropdf417EmptyData#41520";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>5e3){$k[$j++]="bwipp.hibcmicropdf417inputTooLong#41524";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcmicropdf417BadCharacter#41529";$k[$j++]="HIBC MicroPDF417 must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcmicropdf417_barchars,$_.checksum)){$k[$j++]="bwipp.hibcmicropdf417BadCheckDigit#41543";$k[$j++]="Incorrect HIBC MicroPDF417 check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcmicropdf417_barchars,$_.checksum));$p($_.options,"columns",$_.columns);$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_micropdf417();var _r=$k[--$j];$_.args=_r;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibcqrcode(){if(!bwipp_hibcqrcode.globals){var $__=$_;$_=bwipp_hibcqrcode.globals={};$_.hibcqrcode_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcqrcode_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcqrcode.globals);$_.dontdraw=false;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcqrcodeEmptyData#41648";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>3e4){$k[$j++]="bwipp.hibcqrcodeinputTooLong#41652";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcqrcodeBadCharacter#41657";$k[$j++]="HIBC QR Code must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcqrcode_barchars,$_.checksum)){$k[$j++]="bwipp.hibcqrcodeBadCheckDigit#41671";$k[$j++]="Incorrect HIBC QR Code check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcqrcode_barchars,$_.checksum));$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_qrcode();var _p=$k[--$j];$_.args=_p;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibccodablockf(){if(!bwipp_hibccodablockf.globals){var $__=$_;$_=bwipp_hibccodablockf.globals={};$_.hibccodablockf_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibccodablockf_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibccodablockf.globals);$_.dontdraw=false;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibccodablockfEmptyData#41775";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>25e3){$k[$j++]="bwipp.hibccodablockfinputTooLong#41779";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibccodablockfBadCharacter#41784";$k[$j++]="HIBC Codablock F must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibccodablockf_barchars,$_.checksum)){$k[$j++]="bwipp.hibccodablockfBadCheckDigit#41798";$k[$j++]="Incorrect HIBC Codablock F check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibccodablockf_barchars,$_.checksum));$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_codablockf();var _p=$k[--$j];$_.args=_p;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_hibcazteccode(){if(!bwipp_hibcazteccode.globals){var $__=$_;$_=bwipp_hibcazteccode.globals={};$_.hibcazteccode_barchars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";$_.charvals=new Map;for(var _0=0;_0<=42;_0+=1){$p($_.charvals,$G($_.hibcazteccode_barchars,_0,1),_0)}$_.charvals=$_.charvals;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_hibcazteccode.globals);$_.dontdraw=false;$_.validatecheck=false;bwipp_processoptions();var _4=$k[--$j];$_.options=_4;var _5=$k[--$j];$_.barcode=_5;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length==0){$k[$j++]="bwipp.hibcazteccodeEmptyData#41904";$k[$j++]="The data must not be empty";bwipp_raiseerror()}if($_.barcode.length>16e3){$k[$j++]="bwipp.hibcazteccodeinputTooLong#41908";$k[$j++]="The input data is too long";bwipp_raiseerror()}for(var _C=0,_B=$_.barcode.length-1;_C<=_B;_C+=1){if(!$has($_.charvals,$G($_.barcode,_C,1))){$k[$j++]="bwipp.hibcazteccodeBadCharacter#41913";$k[$j++]="HIBC Aztec Code must contain only digits, capital letters, spaces and the symbols -.$/+%";bwipp_raiseerror()}}var _K=$_.barcode.length;if($_.validatecheck){_K=_K-1}$_.barlen=_K;$_.checksum=41;for(var _O=0,_N=$_.barlen-1;_O<=_N;_O+=1){$_.checksum=$f($g($_.charvals,$G($_.barcode,_O,1))+$_.checksum)}$_.checksum=$_.checksum%43;if($_.validatecheck){if($g($_.barcode,$_.barlen)!=$g($_.hibcazteccode_barchars,$_.checksum)){$k[$j++]="bwipp.hibcazteccodeBadCheckDigit#41927";$k[$j++]="Incorrect HIBC Aztec Code check digit provided";bwipp_raiseerror()}$_.barcode=$G($_.barcode,0,$_.barlen)}var _f=$s($_.barlen+2);$P(_f,1,$_.barcode);$_.barcode=_f;$p($_.barcode,0,43);$p($_.barcode,$_.barlen+1,$g($_.hibcazteccode_barchars,$_.checksum));$p($_.options,"validatecheck",false);$_.bwipp_dontdraw=true;$k[$j++]=$_.barcode;$k[$j++]=$_.options;bwipp_azteccode();var _p=$k[--$j];$_.args=_p;$p($_.args,"opt",$_.options);$k[$j++]=$_.args;if($_._render){bwipp_renmatrix()}$_=$__}function bwipp_channelcode(){var $__=$_;$_=Object.assign({},$_,bwipp_channelcode.globals);$_.dontdraw=false;$_.shortfinder=false;$_.includetext=false;$_.includecheck=false;$_.height=1;bwipp_processoptions();var _0=$k[--$j];$_.options=_0;var _1=$k[--$j];$_.barcode=_1;$_._render=!($_.dontdraw||($_.bwipp_dontdraw||bwipp_enabledontdraw));if($_.barcode.length<2||$_.barcode.length>7){$k[$j++]="bwipp.channelcodeBadLength#41997";$k[$j++]="Channel Code must be 2 to 7 digits";bwipp_raiseerror()}$F($_.barcode,function(){var _7=$k[--$j];if(_7<48||_7>57){$k[$j++]="bwipp.channelcodeBadCharacter#42001";$k[$j++]="Channel Code must contain only digits";bwipp_raiseerror()}});if($i($_.barcode)>$g($a([26,292,3493,44072,576688,7742862]),$_.barcode.length-2)){$k[$j++]="bwipp.channelcodeTooBig#42005";$k[$j++]="The Channel Code value is too big for the number of channels";bwipp_raiseerror()}$_.nextb=function(){var _C=$k[$j-1];$k[$j++]=$f($g($_.s,_C)+($g($_.b,_C-1)+($g($_.b,_C-2)+$g($_.s,_C-1))))>4?1:2;if(_C<$_.chan+2){var _N=$k[$j-1];var _P=$k[$j-3];for(var _R=_N,_Q=_P;_R<=_Q;_R+=1){var _U=$k[$j-2];$p($_.b,_U,_R);var _V=$k[$j-3];var _W=$k[$j-4];$k[$j++]=$f(_V-_R)+1;$k[$j++]=_W;$k[$j++]=_U+1;$_.nexts()}}else{var _X=$k[$j-1];var _Z=$k[$j-3];if($le(_X,_Z)){var _c=$k[$j-2];var _d=$k[$j-3];$p($_.b,_c,_d);if($_.value==$_.target){$k[$j++]=Infinity;for(var _g=3;_g<=10;_g+=1){$k[$j++]=$g($_.s,_g);$k[$j++]=$g($_.b,_g)}var _l=$a();$_.out=$G(_l,0,$_.chan*2)}$_.value=$_.value+1}}$j-=4};$_.nexts=function(){var _p=$k[$j-1];if(_p<$_.chan+2){$k[$j++]=1}else{var _s=$k[$j-2];$k[$j++]=_s}var _t=$k[--$j];var _v=$k[$j-2];for(var _x=_t,_w=_v;_x<=_w;_x+=1){var _z=$k[$j-1];$p($_.s,_z,_x);var _10=$k[$j-2];var _11=$k[$j-3];$k[$j++]=$f(_10-_x)+1;$k[$j++]=_11;$k[$j++]=_z;$_.nextb()}$j-=3};$_.encode=function(){var _12=$k[--$j];$_.chan=_12;var _13=$k[--$j];$_.target=_13;$_.value=0;$_.out=-1;$_.b=$a([1,1,1,0,0,0,0,0,0,0,0]);$_.s=$a([0,1,1,0,0,0,0,0,0,0,0]);var _14=$_.chan;$k[$j++]=_14;$k[$j++]=_14;$k[$j++]=3;$_.nexts();$k[$j++]=$_.out};$_.barlen=$_.barcode.length;var _1A=$_.shortfinder?$a([1,1,1,1,1]):$a([1,1,1,1,1,1,1,1,1]);$_.finder=_1A;$k[$j++]="data";$k[$j++]=$i($_.barcode);$k[$j++]=$_.barlen+1;$_.encode();var _1E=$k[--$j];var _1F=$k[--$j];$_[_1F]=_1E;$_.check=$a([]);if($_.includecheck){$_.mod23=$g($a([$a([]),$a([]),$a([13,12,4,9,3,1]),$a([13,2,12,3,18,16,4,1]),$a([11,16,17,8,20,4,10,2,5,1]),$a([1,4,16,18,3,12,2,8,9,13,6,1]),$a([20,16,22,13,15,12,5,4,8,9,21,3,7,1]),$a([2,6,18,8,1,3,9,4,12,13,16,2,6,18,8,1])]),$_.barlen);var _1a=0;for(var _1U=0,_1T=$_.data.length-1;_1U<=_1T;_1U+=1){_1a=$f(_1a+($g($_.data,_1U)-1)*$g($_.mod23,_1U))}$k[$j++]=_1a%23;$k[$j++]=3;$_.encode();var _1b=$k[--$j];$_.check=_1b}$k[$j++]=Infinity;$q($_.finder);$q($_.data);$q($_.check);$_.sbs=$a();$_.txt=$a($_.barlen);for(var _1j=0,_1i=$_.barlen-1;_1j<=_1i;_1j+=1){$_.i=_1j;$p($_.txt,$_.i,$a([$G($_.barcode,$_.i,1),0,0,"",0]))}$k[$j++]=Infinity;var _1q=$_.sbs;var _1v=[];for(var _1s=0,_1t=~~(($_.sbs.length+1)/2);_1s<_1t;_1s++){_1v[_1s]=$_.height}_1v=$a(_1v);var _1z=[];for(var _1x=0,_1y=~~(($_.sbs.length+1)/2);_1x<_1y;_1x++){_1z[_1x]=0}_1z=$a(_1z);$k[$j++]="ren";$k[$j++]="renlinear";$k[$j++]="sbs";$k[$j++]=_1q;$k[$j++]="bhs";$k[$j++]=_1v;$k[$j++]="bbs";$k[$j++]=_1z;$k[$j++]="txt";$k[$j++]=$_.txt;$k[$j++]="text1xalign";$k[$j++]="center";$k[$j++]="borderleft";$k[$j++]=1;$k[$j++]="borderright";$k[$j++]=2;$k[$j++]="opt";$k[$j++]=$_.options;var _22=$d();$k[$j++]=_22;if($_._render){bwipp_renlinear()}$_=$__}function bwipp_renderguards(){var _0=$k[--$j];$_.pixx=_0;if($_.guardwidth<0||$_.guardwidth>20){$k[$j++]="bwipp.renderBadGuardwidth#42124";$k[$j++]="guardwidth must be from 0 to 20";bwipp_raiseerror()}if($_.guardheight<0||$_.guardheight>20){$k[$j++]="bwipp.renderBadGuardheight#42128";$k[$j++]="guardheight must be from 0 to 20";bwipp_raiseerror()}if($_.guardleftpos<-50||$_.guardleftpos>150){$k[$j++]="bwipp.renderBadGuardleftpos#42132";$k[$j++]="guardleftpos must be from -50 to 150";bwipp_raiseerror()}if($_.guardleftypos<-50||$_.guardleftypos>150){$k[$j++]="bwipp.renderBadGuardleftypos#42136";$k[$j++]="guardleftypos must be from -50 to 150";bwipp_raiseerror()}if($_.guardrightpos<-50||$_.guardrightpos>150){$k[$j++]="bwipp.renderBadGuardrightpos#42140";$k[$j++]="guardrightpos must be from -50 to 150";bwipp_raiseerror()}if($_.guardrightypos<-50||$_.guardrightypos>150){$k[$j++]="bwipp.renderBadGuardrightypos#42144";$k[$j++]="guardrightypos must be from -50 to 150";bwipp_raiseerror()}if($_.guardwhitespace){$$.selectfont("OCR-B",$_.guardheight*2);if($_.guardleftpos!=0){$$.moveto(-$_.guardleftpos-2,$f($_.guardleftypos-$_.guardheight/2-1.25));$$.show("<",0,0)}if($_.guardrightpos!=0){$$.moveto($f($_.guardrightpos+$_.pixx-$_.guardwidth)-1,$f($_.guardrightypos-$_.guardheight/2-1.25));$$.show(">",0,0)}}}function bwipp_rendertext(){if(!bwipp_rendertext.globals){var $__=$_;$_=bwipp_rendertext.globals={};var _G=new Map([["offleft",function(){$k[$j++]=$f(-$_.tw-$_.nudge)}],["below",function(){$k[$j++]=$f(-$_.th-$_.nudge)}],["left",function(){$k[$j++]=0}],["bottom",function(){$k[$j++]=0}],["xcenter",function(){$k[$j++]=$f($_.pixx-$_.tw)/2}],["ycenter",function(){$k[$j++]=$f($_.pixy-$_.th)/2}],["right",function(){$k[$j++]=$f($_.pixx-$_.tw)}],["top",function(){$k[$j++]=$f($_.pixy-$_.th)}],["offright",function(){$k[$j++]=$f($_.pixx+$_.nudge)}],["above",function(){$k[$j++]=$f($_.pixy+$_.nudge)}]]);var _X=new Map([["offleft",function(){$k[$j++]=0-$_.nudge}],["below",function(){$k[$j++]=0-$_.nudge}],["left",function(){$k[$j++]=$_.tw}],["bottom",function(){$k[$j++]=$_.th}],["xcenter",function(){$k[$j++]=$f($_.pixx+$_.tw)/2}],["ycenter",function(){$k[$j++]=$f($_.pixy+$_.th)/2}],["right",function(){$k[$j++]=$_.pixx}],["top",function(){$k[$j++]=$_.pixy}],["offright",function(){$k[$j++]=$f($_.pixx+$_.tw+$_.nudge)}],["above",function(){$k[$j++]=$f($_.pixy+$_.th+$_.nudge)}]]);var _o=new Map([["offleft",function(){$k[$j++]=0-$_.nudge}],["below",function(){$k[$j++]=$f(-$_.tw-$_.nudge)}],["left",function(){$k[$j++]=$_.th}],["bottom",function(){$k[$j++]=0}],["xcenter",function(){$k[$j++]=$f($_.pixx+$_.th)/2}],["ycenter",function(){$k[$j++]=$f($_.pixy-$_.tw)/2}],["right",function(){$k[$j++]=$_.pixx}],["top",function(){$k[$j++]=$f($_.pixy-$_.tw)}],["offright",function(){$k[$j++]=$f($_.pixx+$_.th+$_.nudge)}],["above",function(){$k[$j++]=$f($_.pixy+$_.nudge)}]]);var _15=new Map([["offleft",function(){$k[$j++]=$f(-$_.th-$_.nudge)}],["below",function(){$k[$j++]=0-$_.nudge}],["left",function(){$k[$j++]=0}],["bottom",function(){$k[$j++]=$_.tw}],["xcenter",function(){$k[$j++]=$f($_.pixx-$_.th)/2}],["ycenter",function(){$k[$j++]=$f($_.pixy+$_.tw)/2}],["right",function(){$k[$j++]=$f($_.pixx-$_.th)}],["top",function(){$k[$j++]=$_.pixy}],["offright",function(){$k[$j++]=$f($_.pixx+$_.nudge)}],["above",function(){$k[$j++]=$f($_.pixy+$_.tw+$_.nudge)}]]);var _16=new Map([["forward",_G],["backward",_X],["upward",_o],["downward",_15]]);$_.rendertext_positions=_16;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_rendertext.globals);$splay();var _17=$k[--$j];$_.grp=_17;var _18=$k[--$j];$_.pixy=_18;var _19=$k[--$j];$_.pixx=_19;$_.makeerr=function(){var _1A=$k[--$j];$_.makeerr_msg=_1A;var _1B=$k[--$j];$_.makeerr_name=$c($s(128),_1B);if($_.grp==1){$k[$j++]="text"}else{if($_.grp==2){$k[$j++]="extratext"}else{var _1F=$s(5);$P(_1F,0,"text");$p(_1F,4,$_.grp+48);$k[$j++]=_1F}}var _1H=$k[--$j];$_.makeerr_pfx=_1H;var _1K=$s($_.makeerr_pfx.length+6+$_.makeerr_name.length);$P(_1K,0,"bwipp.");$P(_1K,6,$_.makeerr_pfx);$P(_1K,$_.makeerr_pfx.length+6,$_.makeerr_name);var _1Q=$s($_.makeerr_pfx.length+$_.makeerr_msg.length);$P(_1Q,0,$_.makeerr_pfx);$P(_1Q,$_.makeerr_pfx.length,$_.makeerr_msg);$k[$j++]=_1K;$k[$j++]=_1Q};$_.positiontext=function(){var _1U=$k[--$j];$_.th=_1U;var _1V=$k[--$j];$_.tw=_1V;$_.nudge=1;var _1W=$k[--$j];var _1X=$g($_.rendertext_positions,_1W);$k[$j++]=_1X;$k[$j++]=_1X;$r(4,-1);var _1Y=$k[$j-1];if($eq(_1Y,"center")){$k[$j-1]="xcenter"}var _1Z=$k[--$j];var _1a=$k[--$j];if($g(_1a,_1Z)()===true){return true}var _1c=$k[--$j];var _1d=$k[--$j];$k[$j++]=_1c;$k[$j++]=_1d;$r(3,-1);var _1e=$k[$j-1];if($eq(_1e,"center")){$k[$j-1]="ycenter"}var _1f=$k[--$j];var _1g=$k[--$j];if($g(_1g,_1f)()===true){return true}};for(var _1i=0;_1i<1;_1i++){if($_.txt.length>0&&$eq($_.xalign,"unset")&&$eq($_.yalign,"unset")&&$_.content.length==0){if($ne($_.color,"unset")){$$.setcolor($_.color)}$_.s=0;$_.fn="";$_.xoff=0;$_.yoff=0;try{$F($_.txt,function(){var _1q=$k[--$j];$q(_1q);var _1r=$k[$j-1];var _1s=$k[$j-2];if(_1r!=$_.s||$ne(_1s,$_.fn)){var _1v=$k[$j-1];if(_1v<=0){$j-=5;throw Infinity}var _1w=$k[--$j];var _1x=$k[--$j];$_.s=_1w;$_.fn=_1x;$$.selectfont(_1x,_1w);var _1y=$$.stringwidth("0");$_.xoff=-(_1y.w/2);$$.save();$$.newpath();$$.moveto(0,0);$$.charpath("0",false);var _1z=$$.pathbbox();$$.restore();$k[$j++]=_1z.llx;$k[$j++]=_1z.lly;$k[$j++]=_1z.ury;$k[$j++]=_1z.urx;$j--;var _20=$k[--$j];var _21=$k[--$j];$_.yoff=-($f(_21+_20)/2);$j--}else{$j-=2}var _22=$k[--$j];var _23=$k[--$j];$$.moveto(_23,_22);var _24=$k[$j-1];if(_24.length==1){$$.rmoveto($_.xoff,$_.yoff)}var _27=$k[--$j];$$.show(_27,0,0)});var _28=false}catch(e){$k[$j++]=true}if(_28){$k[$j++]="bwipp.fontTooSmall#42285";$k[$j++]="The font size is too small";$k[$j++]=false;break}$k[$j++]=true;break}if($_.content.length==0&&$_.txt.length>0){$k[$j++]=Infinity;$F($_.txt,function(){var _2C=$k[--$j];$F($g(_2C,0))});$_.txtchars=$a();$_.tstr=$s($_.txtchars.length);for(var _2I=0,_2H=$_.txtchars.length-1;_2I<=_2H;_2I+=1){$p($_.tstr,_2I,$g($_.txtchars,_2I))}$_.content=$_.tstr}if($_.content.length==0){$k[$j++]=true;break}if($_.subspace.length!=0&&$_.subspace.length!=1){$k[$j++]="subspaceBad";$k[$j++]="subspace option must be a single character";$_.makeerr();$k[$j++]=false;break}if($_.subspace.length!=0){for(var _2T=0,_2S=$_.content.length-1;_2T<=_2S;_2T+=1){$k[$j++]=_2T;$X($_.subspace,$G($_.content,_2T,1));var _2X=$k[--$j];if(_2X){$j--;$j--;var _2c=$k[$j-2];$p($_.content,_2c,32)}$j-=2}}if($_.split.length!=0&&$_.content.length!=0){if($_.split.length!=1){$k[$j++]="splitBad";$k[$j++]="split option must be a single character";$_.makeerr();$k[$j++]=false;break}$k[$j++]=Infinity;$k[$j++]=$_.content;for(;;){var _2i=$k[--$j];$X(_2i,$_.split);var _2j=$k[--$j];if(_2j){var _2k=$k[--$j];$j--;var _2n=$k[--$j];$k[$j++]=_2k;$k[$j++]=_2n}else{break}}$_.textlines=$a()}else{$_.textlines=$a([$_.content])}if($ne($_.color,"unset")){$$.setcolor($_.color)}$$.selectfont($_.font,$_.size);if($_.content.length==0){$k[$j++]=0}else{$$.save();$$.newpath();$$.moveto(0,0);$$.charpath("0",false);var _2u=$$.pathbbox();$$.restore();$k[$j++]=_2u.ury}var _2v=$k[--$j];$_.textascent=_2v;var _2w=$_.textlines;$k[$j++]="textwidth";$k[$j++]=0;for(var _2x=0,_2y=_2w.length;_2x<_2y;_2x++){var _30=$$.stringwidth($g(_2w,_2x));var _31=$k[--$j];var _32=_30.w;if(_31<_30.w){var _=_32;_32=_31;_31=_}$k[$j++]=_31}var _35=$k[--$j];var _36=$k[--$j];$_[_36]=$f(_35+($_.content.length-1)*$_.gaps);var _38=$_.textlines.length;$_.textheight=$f(_38*$_.textascent+(_38-1)*$_.linegaps);if($eq($_.xalign,"unset")){$_.xalign="center"}if($eq($_.yalign,"unset")){$_.yalign="below"}if($eq($_.xalign,"justify")){if($ne($_.direction,"forward")&&$ne($_.direction,"backward")){$k[$j++]="xjustify";$k[$j++]="xalign=justify requires forward or backward text";$_.makeerr();$k[$j++]=false;break}if($lt($_.textwidth,$_.pixx)&&$_.content.length>1){$_.gaps=$f($_.pixx-$_.textwidth)/($_.content.length-1);var _3N=$eq($_.direction,"forward")?"left":"right";$_.xalign=_3N}else{$_.gaps=0;$_.xalign="center"}}if($eq($_.yalign,"justify")){if($ne($_.direction,"upward")&&$ne($_.direction,"downward")){$k[$j++]="yjustify";$k[$j++]="yalign=justify requires upward or downward text";$_.makeerr();$k[$j++]=false;break}if($lt($_.textwidth,$_.pixy)&&$_.content.length>1){$_.gaps=$f($_.pixy-$_.textwidth)/($_.content.length-1);var _3Z=$eq($_.direction,"upward")?"bottom":"top";$_.yalign=_3Z}else{$_.gaps=0;$_.yalign="center"}}$k[$j++]=$_.xalign;$k[$j++]=$_.yalign;$k[$j++]=$_.direction;$k[$j++]=$_.textwidth;$k[$j++]=$_.textheight;$_.positiontext();var _3g=$k[--$j];var _3h=$k[--$j];$$.moveto(_3h,_3g);$$.rmoveto($_.xoffset,$_.yoffset);$$.save();var _3k=new Map([["forward",0],["upward",90],["backward",180],["downward",270]]);$$.rotate($g(_3k,$_.direction));var _3n=$$.currpos();$$.translate(_3n.x,_3n.y);for(var _3q=0,_3p=$_.textlines.length-1;_3q<=_3p;_3q+=1){$_.i=_3q;$$.moveto(0,0+$f($_.textascent+$_.linegaps)*$_.i);var _3v=$_.textlines;$$.show($g(_3v,_3v.length-$_.i-1),$_.gaps,0)}$$.restore();$k[$j++]=true;break}$_=$__}function bwipp_renlinear(){if($_.bwipjs_rawstack){$_.bwipjs_rawstack.push($k[--$j]);return}if(!bwipp_renlinear.globals){var $__=$_;$_=bwipp_renlinear.globals={};var _0=new Map([["offleft","offleft"],["left","left"],["center","center"],["right","right"],["offright","offright"],["justify","justify"]]);$_.renlinear_xalignopts=_0;var _1=new Map([["above","above"],["top","top"],["center","center"],["bottom","bottom"],["below","below"],["justify","justify"]]);$_.renlinear_yalignopts=_1;var _2=new Map([["forward","forward"],["backward","backward"],["upward","upward"],["downward","downward"]]);$_.renlinear_directionoptions=_2;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_renlinear.globals);$_.sbs=$a([]);$_.bhs=$a([]);$_.bbs=$a([]);$_.txt=$a([]);bwipp_inittextoptions();$_.text1xalign="unset";$_.text1yalign="unset";$_.includetext=false;$_.barcolor="unset";$_.bordercolor="unset";$_.inkspread=null;$_.width=0;$_.barratio=1;$_.spaceratio=1;$_.showborder=false;$_.showbearer=false;$_.borderleft=10;$_.borderright=10;$_.bordertop=1;$_.borderbottom=1;$_.borderwidth=.5;$_.guardwhitespace=false;$_.guardleftpos=0;$_.guardleftypos=0;$_.guardrightpos=0;$_.guardrightypos=0;$_.guardwidth=5;$_.guardheight=7;var _3=$k[--$j];$F(_3,function(){var _4=$k[--$j];var _5=$k[--$j];$_[_5]=_4});$k[$j++]=$_.opt;$del($_,"opt");bwipp_processoptions();$k[$j-1]="textopts";bwipp_grouptextoptions();var _7=$k[--$j];var _8=$k[--$j];$_[_8]=_7;for(var _9=1;_9<=9;_9+=1){$_.textgrp=_9;$k[$j++]=$_.textgrp;$k[$j++]=$g($_.textopts,$_.textgrp-1);bwipp_validatetext();var _E=$k[--$j];if(!_E){bwipp_raiseerror()}}if($_.inkspread<-1||$_.inkspread>1){$k[$j++]="bwipp.renlinearBadInkspread#42500";$k[$j++]="inkspread must be from -1 to 1";bwipp_raiseerror()}if($_.borderleft<-1||$_.borderleft>50){$k[$j++]="bwipp.renlinearBadBorderleft#42504";$k[$j++]="borderleft must be from -1 to 50";bwipp_raiseerror()}if($_.borderright<-1||$_.borderright>50){$k[$j++]="bwipp.renlinearBadBorderright#42508";$k[$j++]="borderright must be from -1 to 50";bwipp_raiseerror()}if($_.bordertop<-1||$_.bordertop>50){$k[$j++]="bwipp.renlinearBadBordertop#42512";$k[$j++]="bordertop must be from -1 to 50";bwipp_raiseerror()}if($_.borderbottom<-1||$_.borderbottom>50){$k[$j++]="bwipp.renlinearBadBorderbottom#42516";$k[$j++]="borderbottom must be from -1 to 50";bwipp_raiseerror()}if($_.borderwidth<0||$_.borderwidth>10){$k[$j++]="bwipp.renlinearBadBorderwidth#42520";$k[$j++]="borderwidth must be from 0 to 10";bwipp_raiseerror()}if($_.barratio<.1||$_.barratio>5){$k[$j++]="bwipp.renlinearBadBarratio#42524";$k[$j++]="barratio must be from 0.1 to 5";bwipp_raiseerror()}if($_.spaceratio<.1||$_.spaceratio>5){$k[$j++]="bwipp.renlinearBadSpaceratio#42528";$k[$j++]="spaceratio must be from 0.1 to 5";bwipp_raiseerror()}if($_.width!=0){if($_.width<.01||$_.width>20){$k[$j++]="bwipp.renlinearBadWidth#42532";$k[$j++]="width must be 0 or from 0.01 to 20";bwipp_raiseerror()}}if($_.guardwidth<0||$_.guardwidth>20){$k[$j++]="bwipp.renlinearBadGuardwidth#42536";$k[$j++]="guardwidth must be from 0 to 20";bwipp_raiseerror()}if($_.guardheight<0||$_.guardheight>20){$k[$j++]="bwipp.renlinearBadGuardheight#42540";$k[$j++]="guardheight must be from 0 to 20";bwipp_raiseerror()}if($_.guardleftpos<-50||$_.guardleftpos>150){$k[$j++]="bwipp.renlinearBadGuardleftpos#42544";$k[$j++]="guardleftpos must be from -50 to 150";bwipp_raiseerror()}if($_.guardleftypos<-50||$_.guardleftypos>150){$k[$j++]="bwipp.renlinearBadGuardleftypos#42548";$k[$j++]="guardleftypos must be from -50 to 150";bwipp_raiseerror()}if($_.guardrightpos<-50||$_.guardrightpos>150){$k[$j++]="bwipp.renlinearBadGuardrightpos#42552";$k[$j++]="guardrightpos must be from -50 to 150";bwipp_raiseerror()}if($_.guardrightypos<-50||$_.guardrightypos>150){$k[$j++]="bwipp.renlinearBadGuardrightypos#42556";$k[$j++]="guardrightypos must be from -50 to 150";bwipp_raiseerror()}if($_.sbs.length!=0){var _l=$_.sbs;$k[$j++]=~~((_l.length+1)/2);if($_.bhs.length<~~((_l.length+1)/2)||$_.bbs.length<~~((_l.length+1)/2)){$k[$j-1]="bwipp.renlinearBadBarHeightArrays#42562";$k[$j++]="The bar height and baseline arrays must be at least as long as the number of bars";bwipp_raiseerror()}$j--}$_.bars=$a(~~(($_.sbs.length+1)/2));$_.pixx=0;$_.pixy=0;for(var _s=0,_r=~~(($_.sbs.length+1)/2)*2-2;_s<=_r;_s+=1){$_.i=_s;if($_.i%2==0){$_.d=$f($g($_.sbs,$_.i)*$_.barratio-$_.barratio)+1;if($g($_.sbs,$_.i)!=0){$_.h=$g($_.bhs,~~($_.i/2))*72;$_.c=$f($_.d/2+$_.pixx);$_.y=$g($_.bbs,~~($_.i/2))*72;$_.w=$f($_.d-$_.inkspread);$p($_.bars,~~($_.i/2),$a([$_.h,$_.c,$_.y,$_.w]));if($f($_.h+$_.y)>$_.pixy){$_.pixy=$f($_.h+$_.y)}}else{$p($_.bars,~~($_.i/2),-1)}}else{$_.d=$f($g($_.sbs,$_.i)*$_.spaceratio-$_.spaceratio)+1}$_.pixx=$f($_.pixx+$_.d)}$$.save();var _1X=$$.currpos();$$.translate(_1X.x,_1X.y);if($_.width!=0){$$.scale($_.width*72/$_.pixx,1)}$_.tl=$a([-$f($_.borderleft+$_.borderwidth/2),$f($_.pixy+$_.bordertop+$_.borderwidth/2)]);$_.tr=$a([$f($_.pixx+$_.borderright+$_.borderwidth/2),$f($_.pixy+$_.bordertop+$_.borderwidth/2)]);$_.bl=$a([-$f($_.borderleft+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)]);$_.br=$a([$f($_.pixx+$_.borderright+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)]);if($_.showbearer){$$.save();$$.newpath();$q($_.bl);var _1x=$k[--$j];var _1y=$k[--$j];$$.moveto(_1y,_1x);$q($_.br);var _20=$k[--$j];var _21=$k[--$j];$$.lineto(_21,_20);$q($_.tl);var _23=$k[--$j];var _24=$k[--$j];$$.moveto(_24,_23);$q($_.tr);var _26=$k[--$j];var _27=$k[--$j];$$.lineto(_27,_26);if($ne($_.bordercolor,"unset")){$$.setcolor($_.bordercolor)}$$.setlinewidth($f($_.borderwidth-$_.inkspread*2));$$.stroke();$$.restore()}else{if($_.showborder){$$.save();$$.newpath();$q($_.bl);var _2E=$k[--$j];var _2F=$k[--$j];$$.moveto(_2F,_2E);var _2J=$a([$_.br,$_.tr,$_.tl]);for(var _2K=0,_2L=_2J.length;_2K<_2L;_2K++){$q($g(_2J,_2K));var _2N=$k[--$j];var _2O=$k[--$j];$$.lineto(_2O,_2N)}$$.closepath();if($ne($_.bordercolor,"unset")){$$.setcolor($_.bordercolor)}$$.setlinewidth($_.borderwidth);$$.stroke();$$.restore()}}$$.save();if($ne($_.barcolor,"unset")){$$.setcolor($_.barcolor)}var _2U=$_.bars;for(var _2V=0,_2W=_2U.length;_2V<_2W;_2V++){var _2X=$g(_2U,_2V);$k[$j++]=_2X;if(_2X!=-1){var _2Y=$k[--$j];$q(_2Y);$$.newpath();var _2Z=$k[--$j];$$.setlinewidth(_2Z);var _2a=$k[--$j];var _2b=$k[--$j];$$.moveto(_2b,_2a);var _2c=$k[--$j];$$.rlineto(0,_2c);$$.stroke()}else{$j--}}$$.restore();for(var _2d=1;_2d<=9;_2d+=1){$_.textgrp=_2d;if($g($g($_.textopts,$_.textgrp-1),"").length!=0||$_.textgrp==1&&$_.txt.length>0&&$_.includetext){var _2n=$g($_.textopts,$_.textgrp-1);$p(_2n,"content",$g(_2n,""));$k[$j++]=_2n;var _2t="txt";var _2s=$_.textgrp==1?$_.txt:$a([]);$p(_2n,_2t,_2s);$k[$j++]=$_.pixx;$k[$j++]=$_.pixy;$k[$j++]=$_.textgrp;$r(4,-1);bwipp_rendertext();var _2y=$k[--$j];if(!_2y){$$.restore();bwipp_raiseerror()}}}$k[$j++]=$_.pixx;bwipp_renderguards();$$.restore();$_=$__}function bwipp_renmatrix(){if($_.bwipjs_rawstack){$_.bwipjs_rawstack.push($k[--$j]);return}if(!bwipp_renmatrix.globals){var $__=$_;$_=bwipp_renmatrix.globals={};var _0=new Map([["offleft","offleft"],["left","left"],["center","center"],["right","right"],["offright","offright"],["justify","justify"]]);$_.renmatrix_xalignopts=_0;var _1=new Map([["above","above"],["top","top"],["center","center"],["bottom","bottom"],["below","below"],["justify","justify"]]);$_.renmatrix_yalignopts=_1;var _2=new Map([["forward","forward"],["backward","backward"],["upward","upward"],["downward","downward"]]);$_.renmatrix_directionoptions=_2;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_renmatrix.globals);$_.width=1;$_.height=1;$_.barcolor="unset";$_.bordercolor="unset";$_.colormap="unset";$_.dotty=false;$_.inkspread=null;$_.inkspreadh=null;$_.inkspreadv=null;$_.txt=$a([]);bwipp_inittextoptions();$_.text1xalign="unset";$_.text1yalign="unset";$_.includetext=false;$_.showborder=false;$_.borderleft=0;$_.borderright=0;$_.bordertop=0;$_.borderbottom=0;$_.borderwidth=.5;$_.guardwhitespace=false;$_.guardleftpos=0;$_.guardleftypos=0;$_.guardrightpos=0;$_.guardrightypos=0;$_.guardwidth=5;$_.guardheight=7;var _3=$k[--$j];$F(_3,function(){var _4=$k[--$j];var _5=$k[--$j];$_[_5]=_4});$k[$j++]=$_.opt;$del($_,"opt");bwipp_processoptions();$k[$j-1]="textopts";bwipp_grouptextoptions();var _7=$k[--$j];var _8=$k[--$j];$_[_8]=_7;if($_.inkspread==null){$_.inkspread=0}if($_.inkspreadh==null){$_.inkspreadh=$_.inkspread}if($_.inkspreadv==null){$_.inkspreadv=$_.inkspread}if(!$has($_,"rowmult")){$k[$j++]=Infinity;for(var _G=0,_H=$_.pixy;_G<_H;_G++){$k[$j++]=1}$_.rowmult=$a()}$_.bufnew=$a($_.pixx*$_.pixy);$_.bufoff=0;$_.pixoff=0;var _L=$_.rowmult;for(var _M=0,_N=_L.length;_M<_N;_M++){for(var _P=0,_Q=$g(_L,_M);_P<_Q;_P++){$P($_.bufnew,$_.bufoff,$G($_.pixs,$_.pixoff,$_.pixx));$_.bufoff=$_.bufoff+$_.pixx}$_.pixoff=$_.pixoff+$_.pixx}$_.pixs=$_.bufnew;for(var _c=1;_c<=9;_c+=1){$_.textgrp=_c;$k[$j++]=$_.textgrp;$k[$j++]=$g($_.textopts,$_.textgrp-1);bwipp_validatetext();var _h=$k[--$j];if(!_h){bwipp_raiseerror()}}if($_.inkspread<-1||$_.inkspread>1){$k[$j++]="bwipp.renmatrixBadInkspread#42780";$k[$j++]="inkspread must be from -1 to 1";bwipp_raiseerror()}if($_.inkspreadh<-1||$_.inkspreadh>1){$k[$j++]="bwipp.renmatrixBadInkspreadh#42784";$k[$j++]="inkspreadh must be from -1 to 1";bwipp_raiseerror()}if($_.inkspreadv<-1||$_.inkspreadv>1){$k[$j++]="bwipp.renmatrixBadInkspreadv#42788";$k[$j++]="inkspreadv must be from -1 to 1";bwipp_raiseerror()}if($_.borderleft<-1||$_.borderleft>50){$k[$j++]="bwipp.renmatrixBadBorderleft#42792";$k[$j++]="borderleft must be from -1 to 50";bwipp_raiseerror()}if($_.borderright<-1||$_.borderright>50){$k[$j++]="bwipp.renmatrixBadBorderright#42796";$k[$j++]="borderright must be from -1 to 50";bwipp_raiseerror()}if($_.bordertop<-1||$_.bordertop>50){$k[$j++]="bwipp.renmatrixBadBordertop#42800";$k[$j++]="bordertop must be from -1 to 50";bwipp_raiseerror()}if($_.borderbottom<-1||$_.borderbottom>50){$k[$j++]="bwipp.renmatrixBadBorderbottom#42804";$k[$j++]="borderbottom must be from -1 to 50";bwipp_raiseerror()}if($_.borderwidth<0||$_.borderwidth>10){$k[$j++]="bwipp.renmatrixBadBorderwidth#42808";$k[$j++]="borderwidth must be from 0 to 10";bwipp_raiseerror()}if($_.width<.01||$_.width>50){$k[$j++]="bwipp.renmatrixBadWidth#42812";$k[$j++]="width must be from 0.01 to 50";bwipp_raiseerror()}if($_.height<.01||$_.height>50){$k[$j++]="bwipp.renmatrixBadHeight#42816";$k[$j++]="height must be from 0.01 to 50";bwipp_raiseerror()}if($_.pixx<=0||$_.pixy<=0){$k[$j++]="bwipp.renmatrixBadDimensions#42820";$k[$j++]="The symbol dimensions must be positive";bwipp_raiseerror()}if($_.pixs.length<$_.pixx*$_.pixy){$k[$j++]="bwipp.renmatrixBadPixelData#42824";$k[$j++]="The pixel data is shorter than the symbol dimensions require";bwipp_raiseerror()}$_.xyget=function(){var _18=$k[--$j];var _19=$k[--$j];$k[$j++]=$g($_.pixs,$f(_19+_18*$_.pixx))};$_.drawlayerdots=function(){$_.pixsorig=$_.pixs;var _1D=$k[--$j];$_.pixs=_1D;$$.newpath();for(var _1G=0,_1F=$_.pixs.length-1;_1G<=_1F;_1G+=1){$_.x=_1G%$_.pixx;$_.y=~~(_1G/$_.pixx);$k[$j++]=$_.x;$k[$j++]=$_.y;$_.xyget();var _1L=$k[--$j];if(_1L==1){$$.moveto($f($_.x+.5),$f($_.pixy-$_.y-.5));$$.arc($f($_.x+.5),$f($_.pixy-$_.y-.5),$f(.5-$_.inkspread),0,360,1)}}$$.fill();$_.pixs=$_.pixsorig};$$.save();var _1U=$$.currpos();$$.translate(_1U.x,_1U.y);$_.inkspread=$_.inkspread/2;$_.inkspreadh=$_.inkspreadh/2;$_.inkspreadv=$_.inkspreadv/2;$$.scale($_.width/$_.pixx*72,$_.height/$_.pixy*72);if($_.showborder){$_.tl=$a([-$f($_.borderleft+$_.borderwidth/2),$f($_.pixy+$_.bordertop+$_.borderwidth/2)]);$_.tr=$a([$f($_.pixx+$_.borderright+$_.borderwidth/2),$f($_.pixy+$_.bordertop+$_.borderwidth/2)]);$_.bl=$a([-$f($_.borderleft+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)]);$_.br=$a([$f($_.pixx+$_.borderright+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)])}else{$_.tl=$a([$f(-$_.borderleft+$_.inkspread),$f($_.pixy+$_.bordertop-$_.inkspread)]);$_.tr=$a([$f($_.pixx+$_.borderright-$_.inkspread),$f($_.pixy+$_.bordertop-$_.inkspread)]);$_.bl=$a([$f(-$_.borderleft+$_.inkspread),$f(-$_.borderbottom+$_.inkspread)]);$_.br=$a([$f($_.pixx+$_.borderright-$_.inkspread),$f(-$_.borderbottom+$_.inkspread)])}if($_.showborder){$$.save();$$.newpath();$q($_.bl);var _2J=$k[--$j];var _2K=$k[--$j];$$.moveto(_2K,_2J);var _2O=$a([$_.br,$_.tr,$_.tl]);for(var _2P=0,_2Q=_2O.length;_2P<_2Q;_2P++){$q($g(_2O,_2P));var _2S=$k[--$j];var _2T=$k[--$j];$$.lineto(_2T,_2S)}$$.closepath();if($ne($_.bordercolor,"unset")){$$.setcolor($_.bordercolor)}$$.setlinewidth($f($_.borderwidth-$_.inkspread*2));$$.stroke();$$.restore()}if($eq($_.colormap,"unset")){$_.colormap=new Map([[1,$_.barcolor]])}var _2a=$_.colormap;for(var _2f=_2a.size,_2e=_2a.keys(),_2d=0;_2d<_2f;_2d++){var _2b=_2e.next().value;var _2c=_2a.get(_2b);$k[$j++]=_2b;$k[$j++]=_2c;if($ne(_2c,"unset")){var _2g=$k[--$j];$$.setcolor(_2g)}else{$j--}var _2h=$k[--$j];$_.key=_2h;var _2o=[];var _2i=$_.pixs;for(var _2j=0,_2k=_2i.length;_2j<_2k;_2j++){_2o[_2j]=$eq($g(_2i,_2j),$_.key)?1:0}_2o=$a(_2o);$k[$j++]=_2o;if($_.dotty){$_.drawlayerdots()}else{var _2q=$k[--$j];$$.drawlayer(_2q,$_.pixx,$_.pixy,$_.inkspreadh,$_.inkspreadv)}}for(var _2r=1;_2r<=9;_2r+=1){$_.textgrp=_2r;if($g($g($_.textopts,$_.textgrp-1),"").length!=0||$_.textgrp==1&&$_.txt.length>0&&$_.includetext){var _31=$g($_.textopts,$_.textgrp-1);$p(_31,"txt",$_.txt);$p(_31,"content",$g(_31,""));$k[$j++]=$_.pixx;$k[$j++]=$_.pixy;$k[$j++]=$_.textgrp;$k[$j++]=_31;bwipp_rendertext();var _37=$k[--$j];if(!_37){$$.restore();bwipp_raiseerror()}}}$k[$j++]=$_.pixx;bwipp_renderguards();$$.restore();$_=$__}function bwipp_renmaximatrix(){if($_.bwipjs_rawstack){$_.bwipjs_rawstack.push($k[--$j]);return}var $__=$_;$_=Object.assign({},$_,bwipp_renmaximatrix.globals);$_.barcolor="unset";$_.bordercolor="unset";$_.inkspread=null;$_.showborder=false;$_.borderleft=0;$_.borderright=0;$_.bordertop=0;$_.borderbottom=0;$_.borderwidth=.5;var _0=$k[--$j];$F(_0,function(){var _1=$k[--$j];var _2=$k[--$j];$_[_2]=_1});$k[$j++]=$_.opt;$del($_,"opt");bwipp_processoptions();$j--;if($_.inkspread==null){$_.inkspread=0}if($_.inkspread<-1||$_.inkspread>1){$k[$j++]="bwipp.renmaximatrixBadInkspread#42974";$k[$j++]="inkspread must be from -1 to 1";bwipp_raiseerror()}if($_.borderleft<-1||$_.borderleft>50){$k[$j++]="bwipp.renmaximatrixBadBorderleft#42978";$k[$j++]="borderleft must be from -1 to 50";bwipp_raiseerror()}if($_.borderright<-1||$_.borderright>50){$k[$j++]="bwipp.renmaximatrixBadBorderright#42982";$k[$j++]="borderright must be from -1 to 50";bwipp_raiseerror()}if($_.bordertop<-1||$_.bordertop>50){$k[$j++]="bwipp.renmaximatrixBadBordertop#42986";$k[$j++]="bordertop must be from -1 to 50";bwipp_raiseerror()}if($_.borderbottom<-1||$_.borderbottom>50){$k[$j++]="bwipp.renmaximatrixBadBorderbottom#42990";$k[$j++]="borderbottom must be from -1 to 50";bwipp_raiseerror()}if($_.borderwidth<0||$_.borderwidth>10){$k[$j++]="bwipp.renmaximatrixBadBorderwidth#42994";$k[$j++]="borderwidth must be from 0 to 10";bwipp_raiseerror()}$$.save();if($_.showborder){$_.tl=$a([-$f($_.borderleft+$_.borderwidth/2),$f(29+$_.bordertop+$_.borderwidth/2)]);$_.tr=$a([$f(30+$_.borderright+$_.borderwidth/2),$f(29+$_.bordertop+$_.borderwidth/2)]);$_.bl=$a([-$f($_.borderleft+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)]);$_.br=$a([$f(30+$_.borderright+$_.borderwidth/2),-$f($_.borderbottom+$_.borderwidth/2)])}else{$_.tl=$a([-$_.borderleft+$_.inkspread,29+$_.bordertop-$_.inkspread]);$_.tr=$a([30+$_.borderright-$_.inkspread,29+$_.bordertop-$_.inkspread]);$_.bl=$a([-$_.borderleft+$_.inkspread,-$_.borderbottom+$_.inkspread]);$_.br=$a([30+$_.borderright-$_.inkspread,-$_.borderbottom+$_.inkspread])}if($_.showborder){$$.save();$$.newpath();$q($_.bl);var _q=$k[--$j];var _r=$k[--$j];$$.moveto(_r,_q);var _v=$a([$_.br,$_.tr,$_.tl]);for(var _w=0,_x=_v.length;_w<_x;_w++){$q($g(_v,_w));var _z=$k[--$j];var _10=$k[--$j];$$.lineto(_10,_z)}$$.closepath();if($ne($_.bordercolor,"unset")){$$.setcolor($_.bordercolor)}$$.setlinewidth($f($_.borderwidth-$_.inkspread*2));$$.stroke();$$.restore()}if($ne($_.barcolor,"unset")){$$.setcolor($_.barcolor)}$$.showmaxicode($_.pixs);$$.restore();$_=$__}function bwipp_validatetext(){if(!bwipp_validatetext.globals){var $__=$_;$_=bwipp_validatetext.globals={};var _0=new Map([["offleft","offleft"],["left","left"],["center","center"],["right","right"],["offright","offright"],["justify","justify"]]);$_.validatetext_xalignopts=_0;var _1=new Map([["above","above"],["top","top"],["center","center"],["bottom","bottom"],["below","below"],["justify","justify"]]);$_.validatetext_yalignopts=_1;var _2=new Map([["forward","forward"],["backward","backward"],["upward","upward"],["downward","downward"]]);$_.validatetext_directionoptions=_2;$_=$__}var $__=$_;$_=Object.assign({},$_,bwipp_validatetext.globals);var _3=$k[--$j];$_.td=_3;var _4=$k[--$j];$_.grp=_4;$k[$j++]="pfx";if($_.grp==1){$k[$j++]="text"}else{if($_.grp==2){$k[$j++]="extratext"}else{var _7=$s(5);$P(_7,0,"text");$p(_7,4,$_.grp+48);$k[$j++]=_7}}var _9=$k[--$j];var _A=$k[--$j];$_[_A]=_9;$_.mkerr=function(){var _B=$k[--$j];$_.msgtail=_B;var _C=$k[--$j];$_.prop=$c($s(128),_C);var _G=$s($_.pfx.length+9+$_.prop.length);$P(_G,0,"bwipp.");$P(_G,6,$_.pfx);$P(_G,$_.pfx.length+6,"Bad");$P(_G,$_.pfx.length+9,$_.prop);var _O=$s($_.pfx.length+$_.prop.length+$_.msgtail.length);$P(_O,0,$_.pfx);$P(_O,$_.pfx.length,$_.prop);$P(_O,$_.pfx.length+$_.prop.length,$_.msgtail);$k[$j++]=_G;$k[$j++]=_O};for(var _V=0;_V<1;_V++){var _X=$g($_.td,"xalign");$k[$j++]=_X;if($ne(_X,"unset")){var _Y=$k[--$j];if(!$has($_.validatetext_xalignopts,_Y)){$k[$j++]="xalign";$k[$j++]=" must be offleft, left, center, right, offright or justify";$_.mkerr();$k[$j++]=false;break}}else{$j--}var _b=$g($_.td,"yalign");$k[$j++]=_b;if($ne(_b,"unset")){var _c=$k[--$j];if(!$has($_.validatetext_yalignopts,_c)){$k[$j++]="yalign";$k[$j++]=" must be above, top, center, bottom, below or justify";$_.mkerr();$k[$j++]=false;break}}else{$j--}if(!$has($_.validatetext_directionoptions,$g($_.td,"direction"))){$k[$j++]="direction";$k[$j++]=" must be forward, backward, upward or downward";$_.mkerr();$k[$j++]=false;break}var _i=$g($_.td,"size");if(_i<=0||_i>=25){$k[$j++]="size";$k[$j++]=" must be greater than zero and less than 25";$_.mkerr();$k[$j++]=false;break}var _k=$g($_.td,"xoffset");if(_k<-150||_k>150){$k[$j++]="xoffset";$k[$j++]=" must be from -150 to 150";$_.mkerr();$k[$j++]=false;break}var _m=$g($_.td,"yoffset");if(_m<-150||_m>150){$k[$j++]="yoffset";$k[$j++]=" must be from -150 to 150";$_.mkerr();$k[$j++]=false;break}var _o=$g($_.td,"gaps");if(_o<-20||_o>20){$k[$j++]="gaps";$k[$j++]=" must be from -20 to 20";$_.mkerr();$k[$j++]=false;break}var _q=$g($_.td,"linegaps");if(_q<0||_q>20){$k[$j++]="linegaps";$k[$j++]=" must be from 0 to 20";$_.mkerr();$k[$j++]=false;break}$k[$j++]=true;break}$_=$__}function bwipp_encode(bwipjs,encoder,text,opts,dontdraw){if(typeof text!=="string"){throw new Error("bwipp.typeError: barcode text not a string ("+text+")")}opts=opts||{};if(typeof opts==="string"){var tmp=opts.split(" ");opts={};for(var i=0;i^03001^02996152382802^029840^029001^0291Z00004951^029UPSN^02906X610^029159^0291234567^0291/1^029^029Y^029634 ALPHA DR^029PITTSBURGH^029PA^029^004",opts:"mode=2 parse"},{bcid:"azteccode",desc:"Aztec Code",text:"This is Aztec Code",opts:"format=full"},{bcid:"azteccodecompact",desc:"Compact Aztec Code",text:"1234",opts:""},{bcid:"aztecrune",desc:"Aztec Runes",text:"1",opts:""},{bcid:"codeone",desc:"Code One",text:"Code One",opts:""},{bcid:"hanxin",desc:"Han Xin Code",text:"This is Han Xin",opts:""},{bcid:"dotcode",desc:"DotCode",text:"This is DotCode",opts:"inkspread=0.16"},{bcid:"ultracode",desc:"Ultracode",text:"Awesome colours!",opts:"eclevel=EC2"},{bcid:"gs1-cc",desc:"GS1 Composite 2D Component",text:"(01)09521234543213(3103)000123",opts:"ccversion=b cccolumns=4"},{bcid:"ean13composite",desc:"EAN-13 Composite",text:"9520123456788|(99)1234-abcd",opts:"includetext"},{bcid:"ean8composite",desc:"EAN-8 Composite",text:"95200002|(21)A12345678",opts:"includetext"},{bcid:"upcacomposite",desc:"UPC-A Composite",text:"012345000058|(99)1234-abcd",opts:"includetext"},{bcid:"upcecomposite",desc:"UPC-E Composite",text:"01234558|(15)021231",opts:"includetext"},{bcid:"databaromnicomposite",desc:"GS1 DataBar Omnidirectional Composite",text:"(01)09521234543213|(11)990102",opts:""},{bcid:"databarstackedcomposite",desc:"GS1 DataBar Stacked Composite",text:"(01)09521234543213|(17)010200",opts:""},{bcid:"databarstackedomnicomposite",desc:"GS1 DataBar Stacked Omnidirectional Composite",text:"(01)03612345678904|(11)990102",opts:""},{bcid:"databartruncatedcomposite",desc:"GS1 DataBar Truncated Composite",text:"(01)09521234543213|(11)990102",opts:""},{bcid:"databarlimitedcomposite",desc:"GS1 DataBar Limited Composite",text:"(01)09521234543213|(21)abcdefghijklmnopqrst",opts:""},{bcid:"databarexpandedcomposite",desc:"GS1 DataBar Expanded Composite",text:"(01)09521234543213(3103)001234|(91)1A2B3C4D5E",opts:""},{bcid:"databarexpandedstackedcomposite",desc:"GS1 DataBar Expanded Stacked Composite",text:"(01)09521234543213(10)ABCDEF|(21)12345678",opts:"segments=4"},{bcid:"gs1-128composite",desc:"GS1-128 Composite",text:"(00)095287654321012346|(02)09521234543213(37)24(10)1234567ABCDEFG",opts:"ccversion=c"},{bcid:"gs1datamatrix",desc:"GS1 Data Matrix",text:"(01)09521234543213(17)120508(10)ABCD1234(410)9501101020917",opts:""},{bcid:"gs1datamatrixrectangular",desc:"GS1 Data Matrix Rectangular",text:"(01)09521234543213(17)120508(10)ABCD1234(410)9501101020917",opts:""},{bcid:"gs1dldatamatrix",desc:"GS1 Digital Link Data Matrix",text:"https://id.gs1.org/01/09521234543213/22/ABC%2D123?99=XYZ-987",opts:"includetext"},{bcid:"gs1qrcode",desc:"GS1 QR Code",text:"(01)09521234543213(8200)http://www.abc.net(10)ABCD1234(410)9501101020917",opts:""},{bcid:"gs1dlqrcode",desc:"GS1 Digital Link QR Code",text:"HTTPS://ID.GS1.ORG/01/09521234543213/22/ABC%2D123?99=XYZ-987",opts:"includetext"},{bcid:"gs1dotcode",desc:"GS1 DotCode",text:"(235)5vBZIF%!>>0;return r*i>>>0}if(t!=null){t=t>>>0;return t*i>>>0}e=e>>>0;return e*i>>>0||0}}var BWIPJS_OPTIONS={bcid:1,text:1,scale:1,scaleX:1,scaleY:1,rotate:1,padding:1,paddingwidth:1,paddingheight:1,paddingtop:1,paddingleft:1,paddingright:1,paddingbottom:1,backgroundcolor:1};function Render(r,t){return _Render(bwipp_lookup(r.bcid),r,t)}function _Render(r,t,e){var i=t.text;if(!i){throw new ReferenceError("bwip-js: bar code text not specified.")}FixupOptions(t);e.setopts&&e.setopts(t);var n=t.scale||2;var a=+t.scaleX||n;var s=+t.scaleY||a;var f=t.rotate||"N";var o=new BWIPJS(e);var h={};for(var v in t){if(!BWIPJS_OPTIONS[v]){h[v]=t[v]}}if(h.alttext){h.includetext=true}if(+h.height&&r!=bwipp_pharmacode2){h.height=h.height/25.4||.5}if(+h.width){h.width=h.width/25.4||0}o.scale(a,s);bwipp_encode(o,r,i,h);return o.render()}function ToRaw(r,t,e){if(arguments.length==1){e=r;r=e.bcid;t=e.text}var i=DrawingBuiltin();i.setopts(e);var n=new BWIPJS(i);var a=bwipp_encode(n,bwipp_lookup(r),t,e,true);var s={pixs:1,pixx:1,pixy:1,sbs:1,bbs:1,bhs:1,txt:1,width:1,height:1};for(var f=0;f=0&&r[e].op=="l";e--);e++;if(eh.x0)n=h.x0;if(sh.y0)a=h.y0;if(fh.x1)n=h.x1;if(sh.y1)a=h.y1;if(fs.x0)t=s.x0;if(is.x1)t=s.x1;if(is.y0)e=s.y0;if(ns.y1)e=s.y1;if(no){var y=s;s=o;o=y}if(f>h){var y=f;f=h;h=y}if(s==o){this.bbox(s-v,f,s+c-v-1,h)}else{this.bbox(s,f-p+g+1,o,h+g)}e++}else if(a.op=="p"){var d=Infinity;var x=Infinity;var _=-Infinity;var m=-Infinity;var b=a.poly;if(b.length!=4){throw new Error("stroke: --not-a-rect--")}for(var n=0,w=b.length-1;n_)_=C;if(Fm)m=F}var v=A(c/2);var g=A(p/2);this.bbox(d-v,x-g,_+v,m+g);i++}else{throw new Error("stroke: --not-a-line--")}}var S=this;this.cmds.push(function(){var r=A(c/2);var t=A(p/2);var e=c-r;var i=p-t;for(var n=0;ns)s=v;if(lf)f=l}this.bbox(e,a+1,s-1,f)}else if(t.op=="e"){this.bbox(t.x-t.rx,t.y-t.ry,t.x+t.rx,t.y+t.ry)}else{throw new Error("fill: --not-a-polygon--")}}var u=this;this.cmds.push(function(){for(var r=0;r9){h++}var t,v,e,i;e=s/2|0;i=f/2|0;t=h-e;if(t&1){t--}v=(4*f|0)-i;var l=t/2-1;var u=(l+1)/2|0;var c=v-2-2*u;this.bbox(0,0,h*30-e,f*3*32+f*4-i);var p=this;this.cmds.push(function(){for(var r=0;re){var n=r;r=e;e=n}if(t>i){var n=t;t=i;i=n}r=M(r);t=M(t);e=A(e);i=A(i);if(this.minx>r)this.minx=r;if(this.maxxt)this.miny=t;if(this.maxy=0;r--){let t=new Uint8Array(i);for(let r=0;rs){for(var f=s+1;fa){F(n,a)}if(v>a){F(o,a)}}else{var h=r[i==t-1?0:i+1][1];var v=r[e==0?t-1:e-1][1];if(h>a){F(o,a)}if(v>a){F(n,a)}}}}},hexagon:function(r,t){var e=r[0][0]|0;var i=r[0][1]|0;var n=r[1][1]-r[0][1]|0;var a=r[2][1]-r[1][1]-1|0;var s=r[2][0]|0;var f=r[4][0]|0;j=parseInt(t.substr(0,2),16);T=parseInt(t.substr(2,2),16);B=parseInt(t.substr(4,2),16);S(e,e+1,i++);for(var o=1;o=1;o--){S(e-2*o,e+1+2*o,i++)}S(e,e+1,i)},ellipse:function(r,t,e,i,n){a(r-e|0,t-i|0,r+e|0,t+i|0,n)},fill:function(r){j=parseInt(r.substr(0,2),16);T=parseInt(r.substr(2,2),16);B=parseInt(r.substr(4,2),16);t();I=[];I.min=Infinity},clip:function(r){if(!C){C=[];C.min=Infinity}var t=I;I=C;for(var e=0,i=r.length;et)I.min=t;if(!I[t]){I[t]=[r]}else{I[t].push(r)}}function S(r,t,e){while(r<=t){L(r++,e,255)}}function t(){var r=I.min;var t=I.length-1;for(var e=r;e<=t;e++){var i=I[e];if(!i){continue}i.sort(function(r,t){return r-t});var n=false;var a=0;for(var s=0,f=i.length;se){r=e;e+=a}if(t>i)t=i;t+=(s+1)/2|0;i=t-f;a*=8*a;f=8*s*s;do{d(c,e,t);y(u,r,t);y(u,r,i);d(c,e,i);l=2*v;if(l>=o){r++;e--;o+=f;v+=o}if(l<=h){t++;i--;h+=a;v+=h}}while(r<=e);while(t-ie)r.min=e;var i=r[e];if(i==null||i>t){r[e]=t}}function d(r,t,e){if(r.min>e)r.min=e;var i=r[e];if(i==null||ir){return!i}else if(s==r){return i}i=!i}return true}function e(r){var t=0;for(var e=0,i=r.length,n=i-1;e0?1:-1}}function DrawingCanvas(e,r){if(IsCanvas(r)){e=r}var i;var n=e.getContext("2d",{willReadFrequently:true});var t=DrawingBuiltin();t.image=f;t.end=o;var a;var s=t.setopts;t.setopts=function(r){a=r;s&&s.call(t,r)};return t;function f(r,t){e.width=r;e.height=t;n.setTransform(1,0,0,1,0,0);if(/^[0-9a-fA-F]{6}$/.test(""+a.backgroundcolor)){n.fillStyle="#"+a.backgroundcolor;n.fillRect(0,0,r,t)}else{n.clearRect(0,0,r,t)}i=n.getImageData(0,0,r,t);return{buffer:i.data,ispng:false}}function o(){n.putImageData(i,0,0);return e}}function DrawingSVG(){var o=0,h=0,v=0,n=0;var l=0,u=0,a=0,c=0;var p;var d="";var f;var t="";var g=[];var y={};var x,_;var m=.55228475-45e-5;var b,w;var k,I;return{setopts(r){p=r},scale(r,t){x=r;_=t},measure(r,t,e,i){e=e|0;i=i|0;var n=FontLib.lookup(t);var a=0;var s=0;var f=0;for(var o=0,h=r.length;o\n";f=null}},clip:function(e){var i='';t="clip"+g.length;g.push(i)},unclip:function(){t=""},text(t,e,r,i,n){t=t|0;e=e|0;var a=t,s=e;var f=FontLib.lookup(n.name);var o=n.width|0;var h=n.height|0;var v=n.dx|0;var l="";for(var u=0;u\n'}else{d+='\n'}}},end(){var r="";for(var t in y){r+=y[t]+'" />\n'}var e=p.backgroundcolor;return'\n'+(g.length?""+g.join("")+"":"")+(/^[0-9A-Fa-f]{6}$/.test(""+e)?'\n':"")+r+d+"\n"}};function C(r,t){r+=k;t+=I;var e=o*r+h*t+v*(b-1)+n*(w-1);var i=l*r+u*t+a*(b-1)+c*(w-1);return""+((e|0)==e?e:e.toFixed(2))+" "+((i|0)==i?i:i.toFixed(2))}}var FontLib=function(){var h=[];var s={};var v={};var l={};var u=0;l.next=l;l.prev=l;return{lookup:t,monochrome:e,getglyph:i,getpaths:n,loadFont:r};function r(r){var t=100;var e=100;var i=null;if(arguments.length==2){i=arguments[1]}else if(arguments.length==3){t=e=+arguments[1]||100;i=arguments[2]}else if(arguments.length==4){e=+arguments[1]||100;t=+arguments[2]||100;i=arguments[3]}else{throw new Error("bwipjs: loadFont: invalid number of arguments")}var n=STBTT.InitFont(toUint8Array(i));n.bwipjs_name=r;n.bwipjs_multx=t;n.bwipjs_multy=e;var a=h.push(n)-1;s[r.toUpperCase()]=a;return a}function t(r){var t=s[r.toUpperCase()];return t===undefined?1:t}function e(r){if(r){throw new Error("bwipjs: monochrome fonts not implemented")}}function i(r,t,e,i){r=r|0;t=t|0;e=+e;i=+i;if(!e||e<8){e=8}if(!i||i<8){i=e}if(r<0||r>=h.length){r=1}if(!t||t<32){t=32}var n=""+r+"c"+t+"w"+e+"h"+i;var a=v[n];if(a){a.prev.next=a.next;a.next.prev=a.prev;var s=l;s.next.prev=a;a.next=s.next;a.prev=s;s.next=a;return a}var f=h[r];var a=STBTT.GetGlyph(f,t,e*f.bwipjs_multx/100,i*f.bwipjs_multy/100)||STBTT.GetGlyph(f,0,e*f.bwipjs_multx/100,i*f.bwipjs_multy/100);a.bytes=a.pixels;a.cachekey=n;a.offset=0;if(u>250){var s=l;var o=s.prev;o.prev.next=s;s.prev=o.prev;o.next=o.prev=null;delete v[o.cachekey]}else{u++}v[n]=a;var s=l;s.next.prev=a;a.next=s.next;a.prev=s;s.next=a;return a}function n(r,t,e,i){r=r|0;t=t|0;e=+e;i=+i;if(!e||e<8){e=8}if(!i||i<8){i=e}if(r<0||r>=h.length){r=1}if(!t||t<32){t=32}var n=h[r];return STBTT.GetPaths(n,t,e*n.bwipjs_multx/100,i*n.bwipjs_multy/100)}}();function LoadFont(){return FontLib.loadFont.apply(FontLib,Array.prototype.slice.call(arguments))}var STBTT=function(){var O=1,D=2,U=3,y=4,F=0,r=1,t=2,S=3,e=0,i=1,n=2,a=3,s=4,o=0,A=1,v=2,j=10;var W=Math.floor;var f=Math.ceil;var N=Math.sqrt;var B=Math.abs;function Y(r){var t=[];for(var e=0;er.length||t<0?r.length:t}function L(r,t){d(r,r.cursor+t)}function E(r,t){var e=0;for(var i=0;ir.length||e>r.length-t){return x()}var i=r.subarray(t,t+e);i.cursor=0;return i}function m(r){var t=r.cursor;var e=E(r,2);if(e){var i=V(r);L(r,i*e);L(r,E(r,i)-1)}return _(r,t,r.cursor-t)}function R(r){var t=V(r);if(t>=32&&t<=246){return t-139}else if(t>=247&&t<=250){return(t-247)*256+V(r)+108}else if(t>=251&&t<=254){return-(t-251)*256-V(r)-108}else if(t==28){return E(r,2)}else if(t==29){return E(r,4)}return 0}function T(r){var t=g(r);if(t==30){L(r,1);while(r.cursor>4==15){break}}}else{R(r)}}function Q(r,t){d(r,0);while(r.cursor=28){T(r)}i=r.cursor;n=V(r);if(n==12){n=V(r)|256}if(n==t){return _(r,e,i-e)}}return _(r,0,0)}function K(r,t,e,i){var n=Q(r,t);for(var a=0;a>0:r}function z(r,t){return r[t]*256+r[t+1]}function J(r,t){var e=r[t]*256+r[t+1];return e&32768?(4294901760|e)>>0:e}function C(r,t){return(r[t]<<24)+(r[t+1]<<16)+(r[t+2]<<8)+r[t+3]}function w(r,t,e){var i=z(r,t+4);var n=t+12;for(var a=0;a=s&&t>1,h=z(e,i+8)>>1,v=z(e,i+10),l=z(e,i+12)>>1,u=i+14,c=u;if(t>65535){return 0}if(t>=z(e,c+l*2)){c+=l*2}c-=2;while(v){h>>=1;var p=z(e,c+h*2);if(t>p){c+=h*2}--v}c+=2;var g,y,d=c-u>>>1;y=z(e,i+14+o*2+2+2*d);if(t>1);var w=C(e,i+16+b*12);var k=C(e,i+16+b*12+4);if(tk){_=b+1}else{var I=C(e,i+16+b*12+8);if(n==12){return I+t-w}else{return I}}}return 0}return 0}function $(r,t,e,i,n,a){r.type=t;r.x=e;r.y=i;r.cx=n;r.cy=a}function nr(r,t){var e,i;if(t>=r.numGlyphs){return-1}if(r.indexToLocFormat>=2){return-1}if(r.indexToLocFormat==0){e=r.glyf+z(r.data,r.loca+t*2)*2;i=r.glyf+z(r.data,r.loca+t*2+2)*2}else{e=r.glyf+C(r.data,r.loca+t*4);i=r.glyf+C(r.data,r.loca+t*4+4)}return e==i?-1:e}function ar(r,t,e){if(r.cff.length){pr(r,t,e)}else{var i=nr(r,t);if(i<0){return 0}e.x0=J(r.data,i+2);e.y0=J(r.data,i+4);e.x1=J(r.data,i+6);e.y1=J(r.data,i+8)}return 1}function sr(r,t,e,i,n,a,s,f,o,h){if(i){if(e){$(r[t++],U,o+s>>1,h+f>>1,o,h)}$(r[t++],U,n,a,s,f)}else{if(e){$(r[t++],U,n,a,o,h)}else{$(r[t++],D,n,a,0,0)}}return t}function fr(r,t){var e=r.data,i=nr(r,t);if(i<0){return null}var n=[];var a=J(e,i);if(a>0){var s=0,f,o,h=0,v,l,u,c=0,p,g=0,y,d,x,_,m,b,w,k;var I=i+10;var G=z(e,i+10+a*2);var C=e.subarray(i+10+a*2+2+G);var F=0;l=1+z(e,I+a*2-2);v=l+2*a;n=Y(v);u=0;f=0;p=v-l;for(o=0;o>1;b=d+n[p+o+1].y>>1}else{m=n[p+o+1].x;b=n[p+o+1].y;++o}}else{m=y;b=d}$(n[A++],O,m,b,0,0);c=0;u=1+z(e,I+h*2);++h}else{if(!(s&1)){if(c){$(n[A++],U,x+y>>1,_+d>>1,x,_)}x=y;_=d;c=1}else{if(c){$(n[A++],U,y,d,x,_)}else{$(n[A++],D,y,d,0,0)}c=0}}}n.length=sr(n,A,c,g,m,b,w,k,x,_)}else if(a==-1){var j=1;var T=i+10;while(j){var s,B,L=[1,0,0,1,0,0];s=J(e,T);T+=2;B=J(e,T);T+=2;if(s&2){if(s&1){L[4]=J(e,T);T+=2;L[5]=J(e,T);T+=2}else{L[4]=V(e,T);T+=1;L[5]=V(e,T);T+=1}}if(s&1<<3){L[0]=L[3]=J(e,T)/16384;T+=2;L[1]=L[2]=0}else if(s&1<<6){L[0]=J(e,T)/16384;T+=2;L[1]=L[2]=0;L[3]=J(e,T)/16384;T+=2}else if(s&1<<7){L[0]=J(e,T)/16384;T+=2;L[1]=J(e,T)/16384;T+=2;L[2]=J(e,T)/16384;T+=2;L[3]=J(e,T)/16384;T+=2}var v=N(L[0]*L[0]+L[1]*L[1]);var l=N(L[2]*L[2]+L[3]*L[3]);var E=Z(r,B);if(E.length>0){for(var o=0,P=E.length;or.max_x||!r.started){r.max_x=t}if(e>r.max_y||!r.started){r.max_y=e}if(t=33900){i=32768}else if(e>=1240){i=1131}t+=i;if(t<0||t>=e){return x()}return G(r,t)}function vr(r,t){var e=r.fdselect;var i,n,a,s,f,o=-1,h;d(e,0);f=V(e);if(f==0){L(e,t);o=V(e)}else if(f==3){i=E(e,2);n=E(e,2);for(h=0;h=n&&t=s){break}q(e,0,u[o]);o++;if(o>=s){break}q(e,u[o],0);o++}break;case 6:if(s<1){return 0}for(;;){if(o>=s){break}q(e,u[o],0);o++;if(o>=s){break}q(e,0,u[o]);o++}break;case 31:if(s<4){return 0}for(;;){if(o+3>=s){break}H(e,u[o],0,u[o+1],u[o+2],s-o==5?u[o+4]:0,u[o+3]);o+=4;if(o+3>=s){break}H(e,0,u[o],u[o+1],u[o+2],u[o+3],s-o==5?u[o+4]:0);o+=4}break;case 30:if(s<4){return 0}for(;;){if(o+3>=s){break}H(e,0,u[o],u[o+1],u[o+2],u[o+3],s-o==5?u[o+4]:0);o+=4;if(o+3>=s){break}H(e,u[o],0,u[o+1],u[o+2],s-o==5?u[o+4]:0,u[o+3]);o+=4}break;case 8:if(s<6){return 0}for(;o+5=s){return 0}q(e,u[o],u[o+1]);break;case 25:if(s<8){return 0}for(;o+1=s){return 0}H(e,u[o],u[o+1],u[o+2],u[o+3],u[o+4],u[o+5]);break;case 26:case 27:if(s<4){return 0}y=0;if(s&1){y=u[o];o++}for(;o+3=10){return 0}c[a++]=g;g=hr(h==10?p:r.gsubrs,f);if(g.length==0){return 0}g.cursor=0;l=0;break;case 11:if(a<=0){return 0}g=c[--a];l=0;break;case 14:or(e);return 1;case 12:var d,x,_,m,b,w,k,I,C,F,S,M,A,j,T=V(g);switch(T){case 34:if(s<7){return 0}d=u[0];x=u[1];I=u[2];_=u[3];m=u[4];b=u[5];w=u[6];H(e,d,0,x,I,_,0);H(e,m,0,b,-I,w,0);break;case 35:if(s<13){return 0}d=u[0];k=u[1];x=u[2];I=u[3];_=u[4];C=u[5];m=u[6];F=u[7];b=u[8];S=u[9];w=u[10];M=u[11];H(e,d,k,x,I,_,C);H(e,m,F,b,S,w,M);break;case 36:if(s<9){return 0}d=u[0];k=u[1];x=u[2];I=u[3];_=u[4];m=u[5];b=u[6];S=u[7];w=u[8];H(e,d,k,x,I,_,0);H(e,m,0,b,S,w,-(k+I+S));break;case 37:if(s<11){return 0}d=u[0];k=u[1];x=u[2];I=u[3];_=u[4];C=u[5];m=u[6];F=u[7];b=u[8];S=u[9];w=M=u[10];A=d+x+_+m+b;j=k+I+C+F+S;if(B(A)>B(j)){M=-j}else{w=-A}H(e,d,k,x,I,_,C);H(e,m,F,b,S,w,M);break;default:return 0}break;default:if(h!=255&&h!=28&&(h<32||h>254)){return 0}if(h==255){y=(E(g,4)|0)/65536}else{L(g,-1);y=(R(g)<<16|0)>>16}if(s>=48){return 0}u[s++]=y;l=0;break}if(l){s=0}}return 0}function ur(){return{started:0,first_x:0,first_y:0,x:0,y:0,min_x:0,max_x:0,min_y:0,max_y:0,vertices:[]}}function cr(r,t){var e=ur();if(lr(r,t,e)){return e.vertices}return null}function pr(r,t,e){var i=ur();var n=lr(r,t,i);e.x0=n?i.min_x:0;e.y0=n?i.min_y:0;e.x1=n?i.max_x:0;e.y1=n?i.max_y:0;return n&&i.vertices?i.vertices.length:0}function Z(r,t){if(!r.cff.length){return fr(r,t)}else{return cr(r,t)}}function I(r,t){var e=z(r.data,r.hhea+34);if(te.ey){return}if(se.ey){a+=(a-i)*(e.ey-s)/(s-n);s=e.ey}if(i<=t&&a<=t){r[t]+=e.direction*(s-n)}else if(i>=t+1&&a>=t+1){}else{r[t]+=e.direction*(s-n)*(1-(i-t+(a-t))/2)}}function mr(r,t,e,i,n){var a=n+1;while(i){if(i.fdx==0){var s=i.fx;if(s=0){M(r,s,i,s,n,s,a);M(t,s+1,i,s,n,s,a)}else{M(t,0,i,s,n,s,a)}}}else{var s=i.fx,f=i.fdx,o=s+f,h,v,l,u,c=i.fdy;if(i.sy>n){h=s+f*(i.sy-n);l=i.sy}else{h=s;l=n}if(i.ey=0&&v>=0&&hv){l=a-(l-n);u=a-(u-n);y=l,l=u,u=y;y=v,v=h,h=y;f=-f;c=-c;y=s,s=o,o=y}d=h|0;x=v|0;_=(d+1-s)*c+n;b=i.direction;w=b*(_-l);r[d]+=w*(1-(h-d+(d+1-d))/2);m=b*c;for(g=d+1;gx){M(r,g,i,s,k,d,F);M(r,g,i,d,F,x,S);M(r,g,i,x,S,I,C)}else if(Ix){M(r,g,i,s,k,x,S);M(r,g,i,x,S,d,F);M(r,g,i,d,F,I,C)}else if(sd){M(r,g,i,s,k,d,F);M(r,g,i,d,F,I,C)}else if(Id){M(r,g,i,s,k,d,F);M(r,g,i,d,F,I,C)}else if(sx){M(r,g,i,s,k,x,S);M(r,g,i,x,S,I,C)}else if(Ix){M(r,g,i,s,k,x,S);M(r,g,i,x,S,I,C)}else{M(r,g,i,s,k,I,C)}}}}i=i.next}}function br(r,t,e,i,n,a){i|=0,n|=0,a|=0;var s=null,f;var o=a,h=0,v;var l=new Float32Array(r.w*2+1);var u=l.subarray(r.w);var c=0;t[e].y0=a+r.h+1;while(h>>0;if(b>255){b=255}r.pixels[h*r.stride+v]=b}y=s;while(y){f=y;f.fx+=f.fdx;y=f.next}++o;++h}}function wr(r,t){for(var e=1;e0&&i.y012){var i,n,a,s,f,o=e>>1,h=r[t].y0=s){break}i=r[t+a];r[t+a]=r[t+s];r[t+s]=i;++a;--s}if(st[x+g].y:t[x+p].y16){return 1}if(l*l+u*u>f){c(r,t,e,(t+i)/2,(e+n)/2,h,v,f,o+1);c(r,h,v,(i+a)/2,(n+s)/2,a,s,f,o+1)}else{r.push({x:a,y:s})}return 1}function X(r,t,e,i,n,a,s,f,o,h,v){var l=i-t,u=n-e,c=a-i,p=s-n,g=f-a,y=o-s,d=f-t,x=o-e,_=N(l*l+u*u)+N(c*c+p*p)+N(g*g+y*y),m=N(d*d+x*x),b=_*_-m*m;if(v>16){return}if(b>h){var w=(t+i)/2,k=(e+n)/2,I=(i+a)/2,C=(n+s)/2,F=(a+f)/2,S=(s+o)/2,M=(w+I)/2,A=(k+C)/2,j=(I+F)/2,T=(C+S)/2,B=(M+j)/2,L=(A+T)/2;X(r,t,e,w,k,M,A,B,L,h,v+1);X(r,B,L,j,T,F,S,f,o,h,v+1)}else{r.push({x:f,y:o})}}function Cr(r,t,e){var i=[],n=t*t,a=-1,s=0,f=0,o=0;for(var h=0,v=r.length;h=0){e[a]=i.length-s}++a;s=i.length;f=r[h].x,o=r[h].y;i.push({x:f,y:o});break;case D:f=r[h].x,o=r[h].y;i.push({x:f,y:o});break;case U:c(i,f,o,r[h].cx,r[h].cy,r[h].x,r[h].y,n,0);f=r[h].x,o=r[h].y;break;case y:X(i,f,o,r[h].cx,r[h].cy,r[h].cx1,r[h].cy1,r[h].x,r[h].y,n,0);f=r[h].x,o=r[h].y;break}}e[a]=i.length-s;return i}function Fr(r,t,e,i,n,a,s,f,o,h){f|=0,o|=0;var v=i>n?n:i,l=[],u=Cr(e,t/v,l);if(u){Ir(r,u,l,i,n,a,s,f,o,h)}}function Sr(r,t,e,i,n,a,s,f,o,h){var v=Z(r,h);var l={};var u=p(r,h,a,s,f,o);l.pixels=t;l.w=e|0;l.h=i|0;l.stride=n|0;if(l.w&&l.h){Fr(l,.35,v,a,s,f,o,u.x0,u.y0,1)}}function Mr(r,t,e,i,n,a,s,f,o,h){Sr(r,t,e,i,n,a,s,f,o,k(r,h))}function Ar(r,t,e){return ir(r,t,e)}function jr(r){var t={};if(!ir(t,r,0)){return null}var e=yr(t);t.ascent=e.ascent;t.descent=e.descent;t.linegap=e.linegap;return t}function Tr(r,t,e,i){i=i||e;var n=e/r.ascent;var a=i/r.ascent;var s=k(r,t);if(!s&&t){return null}var f=Z(r,s);var o=p(r,s,n,a,0,0);var h=o.x1-o.x0;var v=o.y1-o.y0;var l=null;if(h&&v){var u={pixels:l=new Uint8Array(h*v),w:h,h:v,stride:h};Fr(u,.35,f,n,a,0,0,o.x0,o.y0,1)}var c=I(r,s);return{glyph:s,pixels:l,width:h,height:v,top:-o.y0,left:o.x0,advance:W(c.advanceWidth*n)}}function Br(r,t,e,i){i=i||e;var n=e/r.ascent;var a=i/r.ascent;var s=k(r,t);if(!s&&t){return null}var f=Z(r,s);var o=I(r,s);var h=0;var v=0;var l=[];if(f){for(var u=0,c=f.length;uh)h=g.y;if(g.y` before stylesheets load — **no flash of wrong theme** on page load -- Dark mode uses a custom "inked paper" palette (deep charcoal with warm paper-cream text and softened oxblood accent) — intentional character, not generic-dark -- Editorial typography (Fraunces serif + JetBrains Mono), grain overlay, responsive layout down to mobile widths - ---- - -## Quick Start - -### Prerequisites - -- Docker 20.10 or newer -- Docker Compose v2 -- A `.pst` or `.ost` file to index - -### Run (as a daemon — recommended) - -```bash -docker compose up -d --build -``` - -The `-d` flag runs the container in the background (detached). On the first run this takes **3–5 minutes** because `libpff-python` compiles a C extension from source; subsequent startups are nearly instant. - -The container is configured with `restart: unless-stopped`, so it will automatically come back up after a reboot or a crash, and keep running across Docker daemon restarts — until you explicitly stop it. - -Once the container is up: - -- **Main app**: `http://localhost:8000` -- **Admin panel**: `http://localhost:8001` - -### Foreground mode (useful for debugging) - -```bash -docker compose up --build -``` - -This runs the container attached to your terminal so you can see logs inline. Press `Ctrl-C` to stop. - -### Tail the logs - -```bash -docker compose logs -f # follow both streams -docker compose logs -f pst-indexer # explicit service name -``` - -### Check status - -```bash -docker compose ps # is it running? -docker compose top # which processes? -``` - -### Stop - -```bash -docker compose stop # stop, keep data, keep container -docker compose down # stop and remove the container, keep data volume -``` - -### Full reset - -```bash -docker compose down -rm -rf ./data # erases everything including users and the master key -docker compose up -d --build -``` - -### Restart / update - -```bash -# After editing code or pulling new images: -docker compose up -d --build # rebuild & restart with one command -# Or just restart without rebuilding: -docker compose restart -``` - ---- - -## First-Run Setup - -On the very first visit to `http://localhost:8000`, you will be presented with a **setup wizard** that asks you to create the initial administrator: - -1. Choose a username (3–64 characters) -2. Choose a password (8+ characters — use a passphrase you'll remember) -3. Confirm the password - -The setup endpoint is only callable **while the users table is empty**. Once the first admin is created, the endpoint returns `409 Conflict` and is effectively disabled. - -After setup, you are automatically signed in and landed on the main app. - -**Heads up:** the admin panel on port 8001 will refuse to accept any logins until the first admin has been created via the main app's setup wizard. This prevents a race where someone could hit `/api/login` on the admin port before you've set things up. - ---- - -## Running as a Daemon - -By default, this app is designed to **run as a long-lived background service**. The `docker-compose.yml` sets `restart: unless-stopped`, which means once started, the container will: - -- Keep running after you close your terminal -- Automatically restart if the process crashes -- Automatically restart when the host reboots and Docker comes back up -- **Not** restart if you explicitly stop it with `docker compose stop` (hence "unless-stopped") - -### Starting the daemon - -```bash -docker compose up -d --build -``` - -The `-d` flag is short for "detached" — the container is launched in the background and the command returns immediately. Your terminal is free to do other things; the container keeps running. - -### Verifying it's running - -```bash -docker compose ps -``` - -You should see output like: - -``` -NAME IMAGE STATUS PORTS -pst-indexer pst-indexer-pst-indexer Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp, 0.0.0.0:8001->8001/tcp -``` - -The `(healthy)` flag comes from the built-in healthcheck, which probes both `/api/health` endpoints every 30 seconds. - -### Watching logs - -Since the container is detached, you won't see logs in your terminal by default. Attach to them on demand: - -```bash -docker compose logs # print recent logs and exit -docker compose logs -f # follow (live tail), Ctrl-C to detach -docker compose logs -f --tail=100 # follow, starting from the last 100 lines -docker compose logs --since 1h # only logs from the last hour -``` - -Detaching from `-f` does **not** stop the container — it just stops showing you the logs. - -### Stopping - -```bash -docker compose stop # stop the container but keep it around -docker compose start # start it again if previously stopped -docker compose restart # stop and start in one command -docker compose down # stop and remove the container (data volume is kept) -docker compose down -v # stop, remove container, AND remove anonymous volumes -``` - -None of these delete your `./data` directory — that's a bind mount, not a Docker-managed volume, so it's always yours. - -### Survives reboots - -Because `restart: unless-stopped` is set, after a host reboot Docker will automatically start the container as soon as the Docker daemon comes up. Nothing manual required on your part. - -If you ever explicitly stopped it with `docker compose stop`, Docker will *not* auto-start it after reboot (that's the whole point of "unless-stopped"). Run `docker compose start` or `docker compose up -d` to bring it back. - -### Updating to a new version - -```bash -# Pull new source, then: -docker compose up -d --build -``` - -Docker Compose compares the current image to the built one; if they differ, it rebuilds and recreates the container with zero-downtime-style replacement. Your `./data` volume is preserved throughout. - ---- - -## Theming & Dark Mode - -Both the main app and the admin panel support **light and dark themes**, with a sun/moon toggle visible in the masthead (when signed in) and floating in the top-right corner on the login/setup screens. - -### How the toggle works - -- Click the icon to flip between themes -- Choice is saved to `localStorage["pst-theme"]` in your browser -- On a fresh browser (nothing saved), the app respects your operating system's `prefers-color-scheme` setting -- The theme is applied by an inline ` - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py deleted file mode 100644 index e79bf8f..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py +++ /dev/null @@ -1,587 +0,0 @@ -""" -PST Indexer — Admin Panel (port 8001). - -Separate FastAPI instance on its own port so operators can place it behind a -more restrictive firewall rule or VPN than the main app. - -Exposes: -- Admin login (separate session cookie, scoped "admin") -- List, create, delete users -- Toggle admin role -- Reset passwords -- Enroll / confirm / disable MFA for any user (admin-managed flow) -- Self-service MFA for the currently logged-in admin -""" -import io -import os -from pathlib import Path -from typing import Optional - -import qrcode -from fastapi import ( - Depends, FastAPI, HTTPException, Request, Response, status, -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - -import auth -from auth import ( - ADMIN_SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, - User, begin_mfa_enrollment, confirm_mfa_enrollment, count_admins, - count_users, create_session_token, create_user, delete_user, - disable_mfa, find_user_by_id, find_user_by_username, - get_totp_secret, list_users, login_limiter, record_login, - require_admin_user, set_user_role, totp_provisioning_uri, - update_password, verify_password, verify_totp, -) - -ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) -COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" - -app = FastAPI(title="PST Indexer Admin Panel") -app.add_middleware( - CORSMiddleware, - allow_origins=[], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -def _startup(): - auth.init_auth_schema() - - -def _set_admin_cookie(response: Response, token: str): - response.set_cookie( - key=ADMIN_SESSION_COOKIE_NAME, - value=token, - max_age=SESSION_TTL_SECONDS, - httponly=True, - secure=COOKIE_SECURE, - samesite="lax", - path="/", - ) - - -def _clear_admin_cookie(response: Response): - response.delete_cookie(key=ADMIN_SESSION_COOKIE_NAME, path="/") - - -# --------------------------------------------------------------------------- -# Auth -# --------------------------------------------------------------------------- -class LoginPayload(BaseModel): - username: str - password: str - totp_code: Optional[str] = None - - -@app.get("/api/health") -def health(): - return {"status": "ok", "admins": count_admins(), "users": count_users()} - - -@app.get("/api/setup/status") -def setup_status(): - """True if no users exist. Admin UI redirects to main app to complete setup.""" - return {"needs_setup": count_users() == 0} - - -@app.post("/api/login") -def admin_login(payload: LoginPayload, request: Request, response: Response): - ip = request.client.host if request.client else "unknown" - rate_key = f"admin:{ip}:{payload.username.lower()}" - if not login_limiter.check(rate_key): - raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") - - found = find_user_by_username(payload.username) - if not found: - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - user, pw_hash = found - - if not verify_password(pw_hash, payload.password): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - if not user.is_admin: - raise HTTPException(403, "Admin access required") - - if user.mfa_enabled: - if not payload.totp_code: - return JSONResponse( - status_code=401, - content={"detail": "MFA required", "mfa_required": True}, - ) - secret = get_totp_secret(user.id) - if not secret or not verify_totp(secret, payload.totp_code): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid MFA code") - - login_limiter.reset(rate_key) - record_login(user.id) - token = create_session_token(user, scope="admin") - _set_admin_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/logout") -def admin_logout(response: Response): - _clear_admin_cookie(response) - return {"ok": True} - - -@app.get("/api/me") -def me(user: User = Depends(require_admin_user)): - return {"user": user.public_dict()} - - -# --------------------------------------------------------------------------- -# User management -# --------------------------------------------------------------------------- -class CreateUserPayload(BaseModel): - username: str - password: str - role: str = "user" - - -class UpdatePasswordPayload(BaseModel): - password: str - - -class UpdateRolePayload(BaseModel): - role: str - - -@app.get("/api/users") -def api_list_users(_: User = Depends(require_admin_user)): - return [u.public_dict() for u in list_users()] - - -@app.post("/api/users") -def api_create_user( - payload: CreateUserPayload, - admin: User = Depends(require_admin_user), -): - try: - user = create_user(payload.username, payload.password, role=payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - return user.public_dict() - - -@app.delete("/api/users/{user_id}") -def api_delete_user(user_id: str, admin: User = Depends(require_admin_user)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - # Prevent deleting yourself - if target.id == admin.id: - raise HTTPException(400, "You cannot delete your own account") - - # Prevent deleting the last admin - if target.is_admin and count_admins() <= 1: - raise HTTPException(400, "Cannot delete the last remaining admin") - - delete_user(user_id) - return {"deleted": user_id} - - -@app.put("/api/users/{user_id}/role") -def api_set_role( - user_id: str, - payload: UpdateRolePayload, - admin: User = Depends(require_admin_user), -): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - if target.id == admin.id and payload.role != "admin": - raise HTTPException(400, "You cannot demote your own account") - - if (target.is_admin and payload.role != "admin" - and count_admins() <= 1): - raise HTTPException(400, "Cannot demote the last remaining admin") - - try: - set_user_role(user_id, payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - - return find_user_by_id(user_id).public_dict() - - -@app.put("/api/users/{user_id}/password") -def api_update_password( - user_id: str, - payload: UpdatePasswordPayload, - admin: User = Depends(require_admin_user), -): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - try: - update_password(user_id, payload.password) - except ValueError as e: - raise HTTPException(400, str(e)) - return {"ok": True} - - -# --------------------------------------------------------------------------- -# MFA management -# --------------------------------------------------------------------------- -class ConfirmMfaPayload(BaseModel): - totp_code: str - - -@app.post("/api/users/{user_id}/mfa/begin") -def api_begin_mfa(user_id: str, admin: User = Depends(require_admin_user)): - """Provision a new TOTP secret for the target user (admin action). - Returns the secret + otpauth URI so the admin can show it to the user.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret, uri = begin_mfa_enrollment(user_id) - return { - "secret": secret, - "otpauth_uri": uri, - "username": target.username, - "qr_url": f"/api/users/{user_id}/mfa/qr", - } - - -@app.get("/api/users/{user_id}/mfa/qr") -def api_mfa_qr(user_id: str, admin: User = Depends(require_admin_user)): - """Render a PNG QR code of the target user's current (unconfirmed) TOTP URI.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret = get_totp_secret(user_id) - if not secret: - raise HTTPException(400, "No MFA enrollment in progress") - uri = totp_provisioning_uri(secret, target.username) - - qr = qrcode.QRCode(box_size=6, border=2) - qr.add_data(uri) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - buf = io.BytesIO() - img.save(buf, format="PNG") - buf.seek(0) - return StreamingResponse(buf, media_type="image/png") - - -@app.post("/api/users/{user_id}/mfa/confirm") -def api_confirm_mfa( - user_id: str, - payload: ConfirmMfaPayload, - admin: User = Depends(require_admin_user), -): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - if not confirm_mfa_enrollment(user_id, payload.totp_code): - raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") - return find_user_by_id(user_id).public_dict() - - -@app.post("/api/users/{user_id}/mfa/disable") -def api_disable_mfa(user_id: str, admin: User = Depends(require_admin_user)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - disable_mfa(user_id) - return find_user_by_id(user_id).public_dict() - - -# --------------------------------------------------------------------------- -# Folder management (admin) -# --------------------------------------------------------------------------- -import uuid as _uuid -from datetime import datetime, timezone -from pathlib import Path as _Path -from auth import get_db - - -UPLOAD_DIR = _Path(os.environ.get("DATA_DIR", "/app/data")) / "uploads" - - -class AdminFolderPayload(BaseModel): - name: str - - -def _validate_name(name: str) -> str: - name = (name or "").strip() - if not name: - raise HTTPException(400, "Folder name cannot be empty") - if len(name) > 80: - raise HTTPException(400, "Folder name too long (max 80 characters)") - return name - - -@app.get("/api/folders") -def admin_list_folders(_: User = Depends(require_admin_user)): - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.post("/api/folders") -def admin_create_folder(payload: AdminFolderPayload, - admin: User = Depends(require_admin_user)): - name = _validate_name(payload.name) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) - ).fetchone() - if existing: - raise HTTPException(409, "A folder with that name already exists") - folder_id = _uuid.uuid4().hex - conn.execute( - """INSERT INTO pst_folders (id, name, created_at, created_by) - VALUES (?, ?, ?, ?)""", - (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), - ) - conn.commit() - return {"id": folder_id, "name": name, "file_count": 0} - - -@app.put("/api/folders/{folder_id}") -def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, - admin: User = Depends(require_admin_user)): - name = _validate_name(payload.name) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - clash = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", - (name, folder_id), - ).fetchone() - if clash: - raise HTTPException(409, "A folder with that name already exists") - conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", - (name, folder_id)) - conn.commit() - return {"id": folder_id, "name": name} - - -@app.delete("/api/folders/{folder_id}") -def admin_delete_folder(folder_id: str, - admin: User = Depends(require_admin_user)): - """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) - conn.commit() - return {"deleted": folder_id} - - -# --------------------------------------------------------------------------- -# PST file management (admin) -# --------------------------------------------------------------------------- -@app.get("/api/pst-files") -def admin_list_pst_files(_: User = Depends(require_admin_user)): - with get_db() as conn: - rows = conn.execute( - """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, - p.status, p.message_count, p.error, - p.folder_id, f.name AS folder_name, - p.uploaded_by - FROM pst_files p - LEFT JOIN pst_folders f ON f.id = p.folder_id - ORDER BY p.uploaded_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.delete("/api/pst-files/{pst_id}") -def admin_delete_pst(pst_id: str, admin: User = Depends(require_admin_user)): - with get_db() as conn: - row = conn.execute( - "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - raise HTTPException(404, "PST not found") - conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) - conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) - conn.commit() - try: - (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) - except Exception: - pass - return {"deleted": pst_id} - - -class AdminAssignPayload(BaseModel): - folder_id: Optional[str] = None - - -@app.put("/api/pst-files/{pst_id}/folder") -def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, - admin: User = Depends(require_admin_user)): - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_files WHERE id = ?", - (pst_id,)).fetchone(): - raise HTTPException(404, "PST not found") - if payload.folder_id: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (payload.folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", - (payload.folder_id, pst_id)) - conn.commit() - return {"pst_id": pst_id, "folder_id": payload.folder_id} - - -# --------------------------------------------------------------------------- -# Folder permissions (admin-only) -# Controls which user can access which folder. -# Admins ALWAYS have access to every folder regardless of this table — -# these grants are for non-admin users. -# --------------------------------------------------------------------------- -class PermissionToggle(BaseModel): - granted: bool - - -@app.get("/api/permissions") -def admin_list_permissions(_: User = Depends(require_admin_user)): - """Return the full matrix: users, folders, and all grants.""" - with get_db() as conn: - users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() - folders = conn.execute( - "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" - ).fetchall() - grants = conn.execute( - "SELECT user_id, folder_id, granted_at FROM folder_permissions" - ).fetchall() - - from auth import _row_to_user - user_list = [] - for u in users: - pub = _row_to_user(u).public_dict() - user_list.append(pub) - - return { - "users": user_list, - "folders": [dict(f) for f in folders], - "grants": [dict(g) for g in grants], - } - - -@app.get("/api/users/{user_id}/folders") -def admin_user_folders(user_id: str, _: User = Depends(require_admin_user)): - """Folders a given user has been granted.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, p.granted_at - FROM folder_permissions p - JOIN pst_folders f ON f.id = p.folder_id - WHERE p.user_id = ? - ORDER BY f.name COLLATE NOCASE""", - (user_id,), - ).fetchall() - return { - "user": target.public_dict(), - "folders": [dict(r) for r in rows], - } - - -@app.get("/api/folders/{folder_id}/users") -def admin_folder_users(folder_id: str, _: User = Depends(require_admin_user)): - """Users granted on a given folder.""" - with get_db() as conn: - folder = conn.execute( - "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) - ).fetchone() - if not folder: - raise HTTPException(404, "Folder not found") - - grant_rows = conn.execute( - """SELECT user_id, granted_at - FROM folder_permissions WHERE folder_id = ?""", - (folder_id,), - ).fetchall() - - # Hydrate user details (usernames are encrypted — need decrypt via find_user_by_id) - users_out = [] - for r in grant_rows: - u = find_user_by_id(r["user_id"]) - if u: - users_out.append({ - **u.public_dict(), - "granted_at": r["granted_at"], - }) - users_out.sort(key=lambda x: x["username"].lower()) - return { - "folder": dict(folder), - "users": users_out, - } - - -@app.put("/api/permissions/{user_id}/{folder_id}") -def admin_toggle_permission( - user_id: str, - folder_id: str, - payload: PermissionToggle, - admin: User = Depends(require_admin_user), -): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - if target.is_admin: - # Admins implicitly have access to everything; refuse explicit grants - # so the UI doesn't confuse admins with checked-but-meaningless rows. - raise HTTPException( - 400, - "Admins have access to all folders automatically; permissions " - "only apply to non-admin users.", - ) - - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - - if payload.granted: - conn.execute( - """INSERT OR IGNORE INTO folder_permissions - (user_id, folder_id, granted_at, granted_by) - VALUES (?, ?, ?, ?)""", - (user_id, folder_id, - datetime.now(timezone.utc).isoformat(), admin.id), - ) - else: - conn.execute( - "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", - (user_id, folder_id), - ) - conn.commit() - - return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} - - -# --------------------------------------------------------------------------- -# Static admin frontend -# --------------------------------------------------------------------------- -if ADMIN_FRONTEND_DIR.exists(): - app.mount("/", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py deleted file mode 100644 index f462edf..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py +++ /dev/null @@ -1,835 +0,0 @@ -""" -PST Indexer — Main application (port 8000). - -Serves: -- The web UI (login, setup wizard, archive browser) -- REST API for PST upload, indexing, search -- All data-access endpoints require an authenticated session - -First-run behavior: -- If no users exist, the UI routes users to a setup wizard. -- /api/setup is only callable while the users table is empty; after the first - admin is created, the endpoint returns 409. -""" -import os -import re -import sqlite3 -import uuid -import asyncio -from datetime import datetime, timezone -from pathlib import Path -from typing import Optional - -import aiofiles -import pypff -from fastapi import ( - BackgroundTasks, Depends, FastAPI, File, HTTPException, - Request, Response, UploadFile, status, -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, FileResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - -import auth -from auth import ( - SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, - User, count_users, count_admins, create_user, find_user_by_username, - find_user_by_id, get_totp_secret, verify_password, verify_totp, - record_login, create_session_token, login_limiter, require_app_user, - get_db, -) - -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- -DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) -UPLOAD_DIR = DATA_DIR / "uploads" -FRONTEND_DIR = Path(os.environ.get("FRONTEND_DIR", "/app/frontend")) - -UPLOAD_DIR.mkdir(parents=True, exist_ok=True) -DATA_DIR.mkdir(parents=True, exist_ok=True) - -# Cookie security. Flip to True when serving over HTTPS. -COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" - - -# --------------------------------------------------------------------------- -# App DB schema (messages + pst_files) -# --------------------------------------------------------------------------- -def init_app_schema(): - with get_db() as conn: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS pst_folders ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - created_at TEXT NOT NULL, - created_by TEXT - ); - - CREATE TABLE IF NOT EXISTS folder_permissions ( - user_id TEXT NOT NULL, - folder_id TEXT NOT NULL, - granted_at TEXT NOT NULL, - granted_by TEXT, - PRIMARY KEY (user_id, folder_id) - ); - - CREATE INDEX IF NOT EXISTS idx_folder_perms_user ON folder_permissions(user_id); - CREATE INDEX IF NOT EXISTS idx_folder_perms_folder ON folder_permissions(folder_id); - - CREATE TABLE IF NOT EXISTS pst_files ( - id TEXT PRIMARY KEY, - filename TEXT NOT NULL, - original_name TEXT NOT NULL, - uploaded_at TEXT NOT NULL, - size_bytes INTEGER NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - message_count INTEGER DEFAULT 0, - error TEXT, - uploaded_by TEXT, - folder_id TEXT - ); - - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pst_id TEXT NOT NULL, - folder TEXT, - subject TEXT, - sender TEXT, - recipients TEXT, - sent_date TEXT, - body TEXT, - attachments TEXT, - FOREIGN KEY (pst_id) REFERENCES pst_files(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_messages_pst ON messages(pst_id); - CREATE INDEX IF NOT EXISTS idx_messages_sent_date ON messages(sent_date); - CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id); - - CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( - subject, sender, recipients, body, folder, - content='messages', - content_rowid='id', - tokenize='porter unicode61' - ); - - CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN - INSERT INTO messages_fts(rowid, subject, sender, recipients, body, folder) - VALUES (new.id, new.subject, new.sender, new.recipients, new.body, new.folder); - END; - - CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN - INSERT INTO messages_fts(messages_fts, rowid, subject, sender, recipients, body, folder) - VALUES ('delete', old.id, old.subject, old.sender, old.recipients, old.body, old.folder); - END; - """) - # Backfill: add folder_id column to DBs created before this feature existed. - cols = [r["name"] for r in conn.execute("PRAGMA table_info(pst_files)").fetchall()] - if "folder_id" not in cols: - conn.execute("ALTER TABLE pst_files ADD COLUMN folder_id TEXT") - conn.execute("CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id)") - conn.commit() - - -# --------------------------------------------------------------------------- -# Access control helpers — which folders / PSTs is a given user allowed to see? -# -# Rules: -# - Admins see everything. -# - Regular users see only folders they have been explicitly granted, -# plus the implicit "Uncategorized" bucket (folder_id = NULL) — this is -# deliberately shared because otherwise newly-uploaded PSTs would be -# invisible until someone moved them into a permitted folder. -# - A PST is visible iff its folder is visible (or the PST is uncategorized). -# --------------------------------------------------------------------------- -def allowed_folder_ids_for(user) -> set[str]: - """Return the set of folder_ids a user is explicitly permitted on. - For admins this is not used — they see everything unconditionally.""" - with get_db() as conn: - rows = conn.execute( - "SELECT folder_id FROM folder_permissions WHERE user_id = ?", - (user.id,), - ).fetchall() - return {r["folder_id"] for r in rows} - - -def user_can_see_folder(user, folder_id: Optional[str]) -> bool: - """Can this user see content inside this folder? folder_id may be None - (Uncategorized), which is visible to everyone.""" - if user.is_admin: - return True - if folder_id is None: - return True # Uncategorized is shared - return folder_id in allowed_folder_ids_for(user) - - -def user_can_see_pst(user, pst_id: str) -> bool: - if user.is_admin: - return True - with get_db() as conn: - row = conn.execute( - "SELECT folder_id FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - return False - return user_can_see_folder(user, row["folder_id"]) - - -# --------------------------------------------------------------------------- -# PST indexing helpers (unchanged from pre-auth version) -# --------------------------------------------------------------------------- -def _safe_str(value) -> str: - if value is None: - return "" - if isinstance(value, bytes): - try: - return value.decode("utf-8", errors="replace") - except Exception: - return value.decode("latin-1", errors="replace") - return str(value) - - -def _extract_recipients(message) -> str: - parts = [] - try: - headers = _safe_str(getattr(message, "transport_headers", "")) - if headers: - for line in headers.split("\n"): - low = line.lower() - if low.startswith("to:") or low.startswith("cc:") or low.startswith("bcc:"): - parts.append(line.strip()) - except Exception: - pass - return " | ".join(parts) if parts else "" - - -def _extract_body(message) -> str: - for attr in ("plain_text_body", "html_body", "rtf_body"): - try: - body = getattr(message, attr, None) - if body: - text = _safe_str(body) - if attr == "html_body": - text = re.sub(r"<[^>]+>", " ", text) - text = re.sub(r"\s+", " ", text).strip() - if text: - return text - except Exception: - continue - return "" - - -def _extract_sender(message) -> str: - for attr in ("sender_name", "sender_email_address"): - try: - v = _safe_str(getattr(message, attr, "")) - if v: - return v - except Exception: - continue - try: - headers = _safe_str(getattr(message, "transport_headers", "")) - for line in headers.split("\n"): - if line.lower().startswith("from:"): - return line.split(":", 1)[1].strip() - except Exception: - pass - return "" - - -def _extract_date(message) -> Optional[str]: - try: - d = getattr(message, "delivery_time", None) or getattr(message, "client_submit_time", None) - if d: - if isinstance(d, datetime): - return d.isoformat() - return _safe_str(d) - except Exception: - pass - return None - - -def _extract_attachments(message) -> str: - names = [] - try: - count = getattr(message, "number_of_attachments", 0) - for i in range(count): - try: - att = message.get_attachment(i) - name = _safe_str(getattr(att, "name", "") or getattr(att, "long_filename", "")) - if name: - names.append(name) - except Exception: - continue - except Exception: - pass - return " | ".join(names) - - -def _walk_folders(folder, path=""): - try: - name = _safe_str(getattr(folder, "name", "") or "") - except Exception: - name = "" - current_path = f"{path}/{name}" if name else (path or "/") - try: - for i in range(folder.number_of_sub_messages): - try: - msg = folder.get_sub_message(i) - yield current_path, msg - except Exception: - continue - except Exception: - pass - try: - for i in range(folder.number_of_sub_folders): - try: - sub = folder.get_sub_folder(i) - yield from _walk_folders(sub, current_path) - except Exception: - continue - except Exception: - pass - - -def index_pst_file(pst_id: str, file_path: Path): - pff = None - try: - pff = pypff.file() - pff.open(str(file_path)) - root = pff.get_root_folder() - count = 0 - batch = [] - BATCH_SIZE = 500 - - with get_db() as conn: - for folder_path, msg in _walk_folders(root): - try: - batch.append(( - pst_id, folder_path, - _safe_str(getattr(msg, "subject", "")), - _extract_sender(msg), - _extract_recipients(msg), - _extract_date(msg), - _extract_body(msg), - _extract_attachments(msg), - )) - count += 1 - if len(batch) >= BATCH_SIZE: - conn.executemany( - """INSERT INTO messages - (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - batch, - ) - conn.commit() - batch.clear() - except Exception as e: - print(f"[index] skip: {e}") - continue - - if batch: - conn.executemany( - """INSERT INTO messages - (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - batch, - ) - conn.commit() - - conn.execute( - "UPDATE pst_files SET status = ?, message_count = ? WHERE id = ?", - ("ready", count, pst_id), - ) - conn.commit() - - print(f"[index] {pst_id}: {count} messages") - except Exception as e: - print(f"[index] failed: {e}") - with get_db() as conn: - conn.execute( - "UPDATE pst_files SET status = ?, error = ? WHERE id = ?", - ("failed", str(e), pst_id), - ) - conn.commit() - finally: - if pff is not None: - try: - pff.close() - except Exception: - pass - - -# --------------------------------------------------------------------------- -# FastAPI -# --------------------------------------------------------------------------- -app = FastAPI(title="PST Indexer") - -app.add_middleware( - CORSMiddleware, - allow_origins=[], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -def _startup(): - auth.init_auth_schema() - init_app_schema() - - -def _set_session_cookie(response: Response, token: str): - response.set_cookie( - key=SESSION_COOKIE_NAME, - value=token, - max_age=SESSION_TTL_SECONDS, - httponly=True, - secure=COOKIE_SECURE, - samesite="lax", - path="/", - ) - - -def _clear_session_cookie(response: Response): - response.delete_cookie(key=SESSION_COOKIE_NAME, path="/") - - -# ----- Setup / auth endpoints ----- - -class SetupRequest(BaseModel): - username: str - password: str - - -class LoginRequest(BaseModel): - username: str - password: str - totp_code: Optional[str] = None - - -@app.get("/api/setup/status") -def setup_status(): - """Lets the frontend decide whether to show the setup wizard.""" - return {"needs_setup": count_users() == 0} - - -@app.post("/api/setup") -def setup_admin(payload: SetupRequest, response: Response): - if count_users() > 0: - raise HTTPException(status.HTTP_409_CONFLICT, "Setup has already been completed") - - try: - user = create_user(payload.username, payload.password, role="admin") - except ValueError as e: - raise HTTPException(400, str(e)) - - record_login(user.id) - token = create_session_token(user, scope="app") - _set_session_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/login") -def login(payload: LoginRequest, request: Request, response: Response): - ip = request.client.host if request.client else "unknown" - rate_key = f"{ip}:{payload.username.lower()}" - if not login_limiter.check(rate_key): - raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") - - found = find_user_by_username(payload.username) - if not found: - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - user, stored_pw_hash = found - if not verify_password(stored_pw_hash, payload.password): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - if user.mfa_enabled: - if not payload.totp_code: - # Tell the client MFA is required - return JSONResponse( - status_code=401, - content={"detail": "MFA required", "mfa_required": True}, - ) - secret = get_totp_secret(user.id) - if not secret or not verify_totp(secret, payload.totp_code): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid MFA code") - - login_limiter.reset(rate_key) - record_login(user.id) - token = create_session_token(user, scope="app") - _set_session_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/logout") -def logout(response: Response): - _clear_session_cookie(response) - return {"ok": True} - - -@app.get("/api/me") -def me(user: User = Depends(require_app_user)): - return {"user": user.public_dict()} - - -# ----- PST endpoints (all require auth) ----- - -@app.get("/api/health") -def health(): - return {"status": "ok"} - - -@app.get("/api/pst-files") -def list_pst_files(user: User = Depends(require_app_user)): - with get_db() as conn: - if user.is_admin: - rows = conn.execute( - """SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files ORDER BY uploaded_at DESC""" - ).fetchall() - else: - allowed = allowed_folder_ids_for(user) - if allowed: - placeholders = ",".join("?" for _ in allowed) - rows = conn.execute( - f"""SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files - WHERE folder_id IS NULL OR folder_id IN ({placeholders}) - ORDER BY uploaded_at DESC""", - list(allowed), - ).fetchall() - else: - rows = conn.execute( - """SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files WHERE folder_id IS NULL - ORDER BY uploaded_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -# ----- Folder endpoints (shared with admin panel) ----- - -class FolderCreate(BaseModel): - name: str - - -class FolderRename(BaseModel): - name: str - - -class FolderAssign(BaseModel): - folder_id: Optional[str] = None # null = move to uncategorized - - -def _validate_folder_name(name: str) -> str: - name = (name or "").strip() - if not name: - raise HTTPException(400, "Folder name cannot be empty") - if len(name) > 80: - raise HTTPException(400, "Folder name too long (max 80 characters)") - return name - - -@app.get("/api/folders") -def list_folders(user: User = Depends(require_app_user)): - with get_db() as conn: - if user.is_admin: - rows = conn.execute( - """SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""" - ).fetchall() - else: - allowed = allowed_folder_ids_for(user) - if not allowed: - return [] - placeholders = ",".join("?" for _ in allowed) - rows = conn.execute( - f"""SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - WHERE f.id IN ({placeholders}) - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""", - list(allowed), - ).fetchall() - return [dict(r) for r in rows] - - -@app.post("/api/folders") -def create_folder(payload: FolderCreate, user: User = Depends(require_app_user)): - # Only admins can create folders — the admin panel is the authoritative - # management surface. (We still allow creation from the main app sidebar, - # but only for admin users.) - if not user.is_admin: - raise HTTPException(403, "Only administrators can create folders") - name = _validate_folder_name(payload.name) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) - ).fetchone() - if existing: - raise HTTPException(409, "A folder with that name already exists") - folder_id = uuid.uuid4().hex - conn.execute( - """INSERT INTO pst_folders (id, name, created_at, created_by) - VALUES (?, ?, ?, ?)""", - (folder_id, name, datetime.now(timezone.utc).isoformat(), user.id), - ) - conn.commit() - return {"id": folder_id, "name": name, "file_count": 0} - - -@app.put("/api/folders/{folder_id}") -def rename_folder(folder_id: str, payload: FolderRename, - user: User = Depends(require_app_user)): - if not user.is_admin: - raise HTTPException(403, "Only administrators can rename folders") - name = _validate_folder_name(payload.name) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - clash = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", - (name, folder_id), - ).fetchone() - if clash: - raise HTTPException(409, "A folder with that name already exists") - conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", - (name, folder_id)) - conn.commit() - return {"id": folder_id, "name": name} - - -@app.delete("/api/folders/{folder_id}") -def delete_folder(folder_id: str, user: User = Depends(require_app_user)): - """Delete a folder. Any PSTs inside it become uncategorized (folder_id = NULL).""" - if not user.is_admin: - raise HTTPException(403, "Only administrators can delete folders") - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) - conn.commit() - return {"deleted": folder_id} - - -@app.put("/api/pst-files/{pst_id}/folder") -def assign_pst_to_folder(pst_id: str, payload: FolderAssign, - user: User = Depends(require_app_user)): - # Check the user can see the PST's current location AND the destination - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - if payload.folder_id and not user_can_see_folder(user, payload.folder_id): - raise HTTPException(403, "You do not have access to the target folder") - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_files WHERE id = ?", - (pst_id,)).fetchone(): - raise HTTPException(404, "PST not found") - if payload.folder_id: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (payload.folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", - (payload.folder_id, pst_id)) - conn.commit() - return {"pst_id": pst_id, "folder_id": payload.folder_id} - - -@app.post("/api/pst-files") -async def upload_pst( - background_tasks: BackgroundTasks, - file: UploadFile = File(...), - folder_id: Optional[str] = None, - user: User = Depends(require_app_user), -): - if not file.filename: - raise HTTPException(400, "No filename provided") - if not file.filename.lower().endswith((".pst", ".ost")): - raise HTTPException(400, "Only .pst and .ost files are supported") - - # Validate folder if provided (and verify user has access to it) - if folder_id: - with get_db() as conn: - exists = conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone() - if not exists: - raise HTTPException(400, "Target folder does not exist") - if not user_can_see_folder(user, folder_id): - raise HTTPException(403, "You do not have access to the target folder") - - pst_id = uuid.uuid4().hex - stored_name = f"{pst_id}_{file.filename}" - stored_path = UPLOAD_DIR / stored_name - - size = 0 - async with aiofiles.open(stored_path, "wb") as out: - while chunk := await file.read(1024 * 1024): - await out.write(chunk) - size += len(chunk) - - with get_db() as conn: - conn.execute( - """INSERT INTO pst_files - (id, filename, original_name, uploaded_at, size_bytes, status, - uploaded_by, folder_id) - VALUES (?, ?, ?, ?, ?, 'indexing', ?, ?)""", - (pst_id, stored_name, file.filename, - datetime.now(timezone.utc).isoformat(), size, user.id, folder_id), - ) - conn.commit() - - background_tasks.add_task(asyncio.to_thread, index_pst_file, pst_id, stored_path) - return {"id": pst_id, "original_name": file.filename, - "status": "indexing", "size_bytes": size, "folder_id": folder_id} - - -@app.delete("/api/pst-files/{pst_id}") -def delete_pst(pst_id: str, user: User = Depends(require_app_user)): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - with get_db() as conn: - row = conn.execute( - "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - raise HTTPException(404, "PST not found") - conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) - conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) - conn.commit() - try: - (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) - except Exception: - pass - return {"deleted": pst_id} - - -@app.get("/api/pst-files/{pst_id}/folders") -def get_folders(pst_id: str, user: User = Depends(require_app_user)): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - with get_db() as conn: - rows = conn.execute( - """SELECT folder, COUNT(*) as count FROM messages - WHERE pst_id = ? GROUP BY folder ORDER BY folder""", - (pst_id,), - ).fetchall() - return [dict(r) for r in rows] - - -def _escape_fts(query: str) -> str: - tokens = [] - for raw in query.split(): - if re.fullmatch(r"[A-Za-z0-9]+", raw): - tokens.append(f'"{raw}"*') - else: - sanitized = raw.replace('"', '""') - tokens.append(f'"{sanitized}"') - return " ".join(tokens) - - -@app.get("/api/pst-files/{pst_id}/messages") -def search_messages( - pst_id: str, - q: Optional[str] = None, - folder: Optional[str] = None, - limit: int = 50, - offset: int = 0, - user: User = Depends(require_app_user), -): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - limit = max(1, min(limit, 200)) - offset = max(0, offset) - - with get_db() as conn: - p = conn.execute("SELECT id FROM pst_files WHERE id = ?", (pst_id,)).fetchone() - if not p: - raise HTTPException(404, "PST not found") - - params = [pst_id] - - if q and q.strip(): - fts_q = _escape_fts(q.strip()) - base = """ - FROM messages m - JOIN messages_fts f ON f.rowid = m.id - WHERE m.pst_id = ? AND messages_fts MATCH ? - """ - params.append(fts_q) - if folder: - base += " AND m.folder = ?" - params.append(folder) - - total = conn.execute("SELECT COUNT(*) as c " + base, params).fetchone()["c"] - - data_sql = ( - "SELECT m.id, m.folder, m.subject, m.sender, m.recipients, m.sent_date, " - " m.attachments, snippet(messages_fts, 3, '', '', '…', 24) AS snippet " - + base + " ORDER BY rank LIMIT ? OFFSET ?" - ) - rows = conn.execute(data_sql, params + [limit, offset]).fetchall() - else: - where = ["m.pst_id = ?"] - if folder: - where.append("m.folder = ?") - params.append(folder) - where_sql = " AND ".join(where) - total = conn.execute( - f"SELECT COUNT(*) as c FROM messages m WHERE {where_sql}", params - ).fetchone()["c"] - rows = conn.execute( - f"""SELECT m.id, m.folder, m.subject, m.sender, m.recipients, - m.sent_date, m.attachments, substr(m.body, 1, 200) AS snippet - FROM messages m WHERE {where_sql} - ORDER BY m.sent_date DESC LIMIT ? OFFSET ?""", - params + [limit, offset], - ).fetchall() - - return {"total": total, "limit": limit, "offset": offset, - "items": [dict(r) for r in rows]} - - -@app.get("/api/messages/{message_id}") -def get_message(message_id: int, user: User = Depends(require_app_user)): - with get_db() as conn: - row = conn.execute( - """SELECT id, pst_id, folder, subject, sender, recipients, - sent_date, body, attachments - FROM messages WHERE id = ?""", - (message_id,), - ).fetchone() - if not row: - raise HTTPException(404, "Message not found") - if not user_can_see_pst(user, row["pst_id"]): - raise HTTPException(404, "Message not found") - return dict(row) - - -# --------------------------------------------------------------------------- -# Static frontend (anonymous access — but index.html enforces auth client-side -# AND the APIs enforce it server-side, which is what actually matters) -# --------------------------------------------------------------------------- -if FRONTEND_DIR.exists(): - app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend") diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py deleted file mode 100644 index eedd118..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py +++ /dev/null @@ -1,513 +0,0 @@ -""" -auth.py — Shared authentication & encryption module. - -Responsibilities: -- Derive or generate a master secret key (persisted to disk with chmod 600) -- Provide Fernet-based encryption for user PII at rest -- Argon2id password hashing (constant-time verification) -- TOTP secret generation, QR provisioning URI, 6-digit code verification -- JWT session tokens (signed with master key) -- User CRUD on the shared SQLite database -- FastAPI dependencies to enforce authentication and admin role -""" -from __future__ import annotations - -import base64 -import hashlib -import hmac -import os -import secrets -import sqlite3 -import time -from contextlib import contextmanager -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Optional - -import jwt -import pyotp -from argon2 import PasswordHasher -from argon2.exceptions import VerifyMismatchError, InvalidHashError -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from fastapi import Depends, HTTPException, Request, status - -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- -DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) -DATA_DIR.mkdir(parents=True, exist_ok=True) - -SECRET_KEY_PATH = DATA_DIR / "secret.key" -DB_PATH = DATA_DIR / "pst_index.db" - -SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", 60 * 60 * 8)) # 8 hours -SESSION_COOKIE_NAME = "pst_session" -ADMIN_SESSION_COOKIE_NAME = "pst_admin_session" - -JWT_ALGORITHM = "HS256" - -# --------------------------------------------------------------------------- -# Master secret -# --------------------------------------------------------------------------- -def _load_or_create_master_secret() -> bytes: - """Return a 32-byte master secret. Created on first run, persisted to disk.""" - # Allow overriding via env var (useful for orchestration); still persist so - # restarts without the env var keep working. - env_secret = os.environ.get("MASTER_SECRET") - if env_secret: - return hashlib.sha256(env_secret.encode("utf-8")).digest() - - if SECRET_KEY_PATH.exists(): - data = SECRET_KEY_PATH.read_bytes() - if len(data) == 32: - return data - # Legacy or truncated: rewrap - return hashlib.sha256(data).digest() - - secret = secrets.token_bytes(32) - SECRET_KEY_PATH.write_bytes(secret) - try: - os.chmod(SECRET_KEY_PATH, 0o600) - except Exception: - pass - return secret - - -MASTER_SECRET: bytes = _load_or_create_master_secret() - - -def _derive_subkey(info: bytes, length: int = 32) -> bytes: - """Derive a sub-key from the master secret using HKDF-SHA256.""" - return HKDF( - algorithm=hashes.SHA256(), - length=length, - salt=b"pst-indexer-salt-v1", - info=info, - ).derive(MASTER_SECRET) - - -# Key used for encrypting user PII at rest -_FERNET_KEY = base64.urlsafe_b64encode(_derive_subkey(b"fernet-user-pii")) -_FERNET = Fernet(_FERNET_KEY) - -# Key used for JWT signatures -_JWT_KEY = _derive_subkey(b"jwt-sessions") - -# Key used for deterministic username lookup hashes (so we can index without -# exposing plaintext usernames in the DB). -_USERNAME_HMAC_KEY = _derive_subkey(b"username-lookup") - - -# --------------------------------------------------------------------------- -# Encryption helpers -# --------------------------------------------------------------------------- -def encrypt(plaintext: str) -> str: - """Fernet-encrypt a string; returns base64 ciphertext.""" - if plaintext is None: - return "" - return _FERNET.encrypt(plaintext.encode("utf-8")).decode("utf-8") - - -def decrypt(ciphertext: str) -> str: - """Fernet-decrypt. Returns empty string for empty input.""" - if not ciphertext: - return "" - try: - return _FERNET.decrypt(ciphertext.encode("utf-8")).decode("utf-8") - except InvalidToken: - raise ValueError("Failed to decrypt — master key may have changed") - - -def username_lookup_hash(username: str) -> str: - """Deterministic HMAC-SHA256 over the normalized username, hex-encoded.""" - normalized = username.strip().lower().encode("utf-8") - return hmac.new(_USERNAME_HMAC_KEY, normalized, hashlib.sha256).hexdigest() - - -# --------------------------------------------------------------------------- -# Password hashing -# --------------------------------------------------------------------------- -_hasher = PasswordHasher( - time_cost=3, - memory_cost=64 * 1024, # 64 MiB - parallelism=2, -) - - -def hash_password(password: str) -> str: - return _hasher.hash(password) - - -def verify_password(stored_hash: str, password: str) -> bool: - try: - _hasher.verify(stored_hash, password) - return True - except (VerifyMismatchError, InvalidHashError): - return False - except Exception: - return False - - -# --------------------------------------------------------------------------- -# TOTP (MFA) -# --------------------------------------------------------------------------- -def generate_totp_secret() -> str: - return pyotp.random_base32() - - -def totp_provisioning_uri(secret: str, username: str, issuer: str = "PST Archive") -> str: - return pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer) - - -def verify_totp(secret: str, code: str) -> bool: - if not secret or not code: - return False - try: - return pyotp.TOTP(secret).verify(code.strip(), valid_window=1) - except Exception: - return False - - -# --------------------------------------------------------------------------- -# Database -# --------------------------------------------------------------------------- -@contextmanager -def get_db(): - conn = sqlite3.connect(str(DB_PATH)) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA foreign_keys = ON") - try: - yield conn - finally: - conn.close() - - -def init_auth_schema(): - with get_db() as conn: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username_hash TEXT NOT NULL UNIQUE, - username_enc TEXT NOT NULL, - password_hash_enc TEXT NOT NULL, - totp_secret_enc TEXT, - mfa_enabled INTEGER NOT NULL DEFAULT 0, - role TEXT NOT NULL DEFAULT 'user', - created_at TEXT NOT NULL, - last_login TEXT - ); - - CREATE INDEX IF NOT EXISTS idx_users_username_hash ON users(username_hash); - CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); - """) - conn.commit() - - -# --------------------------------------------------------------------------- -# User model -# --------------------------------------------------------------------------- -@dataclass -class User: - id: str - username: str - role: str - mfa_enabled: bool - created_at: str - last_login: Optional[str] - - @property - def is_admin(self) -> bool: - return self.role == "admin" - - def public_dict(self) -> dict: - return { - "id": self.id, - "username": self.username, - "role": self.role, - "mfa_enabled": self.mfa_enabled, - "created_at": self.created_at, - "last_login": self.last_login, - } - - -def _row_to_user(row: sqlite3.Row) -> User: - return User( - id=row["id"], - username=decrypt(row["username_enc"]), - role=row["role"], - mfa_enabled=bool(row["mfa_enabled"]), - created_at=row["created_at"], - last_login=row["last_login"], - ) - - -def count_users() -> int: - with get_db() as conn: - return conn.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] - - -def count_admins() -> int: - with get_db() as conn: - return conn.execute("SELECT COUNT(*) AS c FROM users WHERE role='admin'").fetchone()["c"] - - -def find_user_by_username(username: str) -> Optional[tuple[User, str]]: - """Return (User, decrypted_password_hash) if found, else None.""" - uh = username_lookup_hash(username) - with get_db() as conn: - row = conn.execute( - "SELECT * FROM users WHERE username_hash = ?", (uh,) - ).fetchone() - if not row: - return None - user = _row_to_user(row) - try: - pw_hash = decrypt(row["password_hash_enc"]) - except ValueError: - return None - return user, pw_hash - - -def find_user_by_id(user_id: str) -> Optional[User]: - with get_db() as conn: - row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() - if not row: - return None - return _row_to_user(row) - - -def get_totp_secret(user_id: str) -> Optional[str]: - with get_db() as conn: - row = conn.execute( - "SELECT totp_secret_enc FROM users WHERE id = ?", (user_id,) - ).fetchone() - if not row or not row["totp_secret_enc"]: - return None - try: - return decrypt(row["totp_secret_enc"]) - except ValueError: - return None - - -def list_users() -> list[User]: - with get_db() as conn: - rows = conn.execute( - "SELECT * FROM users ORDER BY created_at ASC" - ).fetchall() - return [_row_to_user(r) for r in rows] - - -def create_user(username: str, password: str, role: str = "user") -> User: - username = username.strip() - if not username or len(username) < 3: - raise ValueError("Username must be at least 3 characters") - if len(username) > 64: - raise ValueError("Username too long") - if role not in ("user", "admin"): - raise ValueError("Invalid role") - if len(password) < 8: - raise ValueError("Password must be at least 8 characters") - - uh = username_lookup_hash(username) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM users WHERE username_hash = ?", (uh,) - ).fetchone() - if existing: - raise ValueError("Username already taken") - - user_id = secrets.token_hex(16) - now = datetime.now(timezone.utc).isoformat() - pw_hash = hash_password(password) - - conn.execute( - """INSERT INTO users - (id, username_hash, username_enc, password_hash_enc, - totp_secret_enc, mfa_enabled, role, created_at, last_login) - VALUES (?, ?, ?, ?, NULL, 0, ?, ?, NULL)""", - (user_id, uh, encrypt(username), encrypt(pw_hash), role, now), - ) - conn.commit() - - return User( - id=user_id, username=username, role=role, mfa_enabled=False, - created_at=now, last_login=None, - ) - - -def delete_user(user_id: str) -> None: - with get_db() as conn: - # Also clean up any folder permission grants this user had - conn.execute("DELETE FROM folder_permissions WHERE user_id = ?", (user_id,)) - conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) - conn.commit() - - -def set_user_role(user_id: str, role: str) -> None: - if role not in ("user", "admin"): - raise ValueError("Invalid role") - with get_db() as conn: - conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id)) - conn.commit() - - -def update_password(user_id: str, new_password: str) -> None: - if len(new_password) < 8: - raise ValueError("Password must be at least 8 characters") - pw_hash = hash_password(new_password) - with get_db() as conn: - conn.execute( - "UPDATE users SET password_hash_enc = ? WHERE id = ?", - (encrypt(pw_hash), user_id), - ) - conn.commit() - - -def begin_mfa_enrollment(user_id: str) -> tuple[str, str]: - """Generate a new TOTP secret (not yet enabled) and return (secret, provisioning_uri).""" - user = find_user_by_id(user_id) - if not user: - raise ValueError("User not found") - secret = generate_totp_secret() - # Stash it but leave mfa_enabled = 0 until confirmed - with get_db() as conn: - conn.execute( - "UPDATE users SET totp_secret_enc = ?, mfa_enabled = 0 WHERE id = ?", - (encrypt(secret), user_id), - ) - conn.commit() - uri = totp_provisioning_uri(secret, user.username) - return secret, uri - - -def confirm_mfa_enrollment(user_id: str, code: str) -> bool: - secret = get_totp_secret(user_id) - if not secret or not verify_totp(secret, code): - return False - with get_db() as conn: - conn.execute("UPDATE users SET mfa_enabled = 1 WHERE id = ?", (user_id,)) - conn.commit() - return True - - -def disable_mfa(user_id: str) -> None: - with get_db() as conn: - conn.execute( - "UPDATE users SET mfa_enabled = 0, totp_secret_enc = NULL WHERE id = ?", - (user_id,), - ) - conn.commit() - - -def record_login(user_id: str) -> None: - with get_db() as conn: - conn.execute( - "UPDATE users SET last_login = ? WHERE id = ?", - (datetime.now(timezone.utc).isoformat(), user_id), - ) - conn.commit() - - -# --------------------------------------------------------------------------- -# JWT sessions -# --------------------------------------------------------------------------- -def create_session_token(user: User, scope: str = "app") -> str: - """scope='app' for main app, scope='admin' for admin panel.""" - now = datetime.now(timezone.utc) - payload = { - "sub": user.id, - "role": user.role, - "scope": scope, - "iat": int(now.timestamp()), - "exp": int((now + timedelta(seconds=SESSION_TTL_SECONDS)).timestamp()), - } - return jwt.encode(payload, _JWT_KEY, algorithm=JWT_ALGORITHM) - - -def verify_session_token(token: str, expected_scope: str) -> Optional[dict]: - try: - payload = jwt.decode(token, _JWT_KEY, algorithms=[JWT_ALGORITHM]) - if payload.get("scope") != expected_scope: - return None - return payload - except jwt.PyJWTError: - return None - - -# --------------------------------------------------------------------------- -# FastAPI dependencies -# --------------------------------------------------------------------------- -def _extract_token(request: Request, cookie_name: str) -> Optional[str]: - # Prefer cookie, fall back to Authorization header - token = request.cookies.get(cookie_name) - if token: - return token - auth = request.headers.get("authorization", "") - if auth.lower().startswith("bearer "): - return auth[7:].strip() - return None - - -def require_user(scope: str, cookie_name: str): - def _dep(request: Request) -> User: - token = _extract_token(request, cookie_name) - if not token: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated") - payload = verify_session_token(token, expected_scope=scope) - if not payload: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired session") - user = find_user_by_id(payload["sub"]) - if not user: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User no longer exists") - return user - return _dep - - -def require_admin(scope: str, cookie_name: str): - base = require_user(scope, cookie_name) - def _dep(request: Request) -> User: - user = base(request) - if not user.is_admin: - raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin role required") - return user - return _dep - - -# Pre-wired dependencies for the two app scopes -require_app_user = require_user("app", SESSION_COOKIE_NAME) -require_admin_user = require_admin("admin", ADMIN_SESSION_COOKIE_NAME) - - -# --------------------------------------------------------------------------- -# Simple rate limiter (per-IP, per-username) to slow brute-force attempts -# --------------------------------------------------------------------------- -class RateLimiter: - def __init__(self, window_seconds: int = 300, max_attempts: int = 10): - self.window = window_seconds - self.max = max_attempts - self._attempts: dict[str, list[float]] = {} - - def check(self, key: str) -> bool: - now = time.time() - cutoff = now - self.window - arr = [t for t in self._attempts.get(key, []) if t > cutoff] - if len(arr) >= self.max: - self._attempts[key] = arr - return False - return True - - def record(self, key: str) -> None: - now = time.time() - cutoff = now - self.window - arr = [t for t in self._attempts.get(key, []) if t > cutoff] - arr.append(now) - self._attempts[key] = arr - - def reset(self, key: str) -> None: - self._attempts.pop(key, None) - - -login_limiter = RateLimiter(window_seconds=300, max_attempts=10) diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt deleted file mode 100644 index 9cbd286..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -python-multipart==0.0.12 -libpff-python==20231205 -aiofiles==24.1.0 -argon2-cffi==23.1.0 -pyotp==2.9.0 -cryptography==43.0.1 -PyJWT==2.9.0 -qrcode[pil]==7.4.2 diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml deleted file mode 100644 index 61385c9..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml +++ /dev/null @@ -1,20 +0,0 @@ -services: - pst-indexer: - build: . - container_name: pst-indexer - ports: - - "8000:8000" # Main app - - "8001:8001" # Admin panel - volumes: - - ./data:/app/data - restart: unless-stopped - environment: - - DATA_DIR=/app/data - # Set to "true" when serving over HTTPS - - COOKIE_SECURE=false - # Session lifetime in seconds (default 8 hours) - - SESSION_TTL_SECONDS=28800 - # Optional: provide your own master secret. If set, this becomes the - # root key for all encryption. If omitted, a random key is generated - # on first run and persisted in ./data/secret.key (chmod 600). - # - MASTER_SECRET=change-me-to-a-long-random-string diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh deleted file mode 100644 index fb9fe33..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Start both the main app (port 8000) and the admin panel (port 8001). -# If either exits, kill the whole container so Docker's restart policy fires. - -set -e - -cleanup() { - echo "Shutting down…" - kill -TERM "$MAIN_PID" "$ADMIN_PID" 2>/dev/null || true - wait - exit 0 -} -trap cleanup SIGINT SIGTERM - -cd /app/backend - -echo "[start] Main app on :8000" -uvicorn app:app --host 0.0.0.0 --port 8000 --proxy-headers & -MAIN_PID=$! - -echo "[start] Admin panel on :8001" -uvicorn admin_app:app --host 0.0.0.0 --port 8001 --proxy-headers & -ADMIN_PID=$! - -# Wait for either to exit -wait -n "$MAIN_PID" "$ADMIN_PID" -EXIT_CODE=$? -echo "[exit] One of the services exited with code $EXIT_CODE; stopping the other" -kill -TERM "$MAIN_PID" "$ADMIN_PID" 2>/dev/null || true -wait -exit $EXIT_CODE diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js deleted file mode 100644 index fdc6cd8..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js +++ /dev/null @@ -1,869 +0,0 @@ -/* ============================================================ - PST Archive — frontend logic - - Setup wizard (first run) - - Login (+ MFA) - - Main app: upload, index, search, view - ============================================================ */ - -const API = "/api"; - -const state = { - activePstId: null, - activePstName: "", - folder: "", - query: "", - limit: 25, - offset: 0, - total: 0, - refreshTimer: null, - user: null, - folders: [], // [{id, name, file_count}, ...] - psts: [], // last loaded pst list - collapsedFolders: new Set(), // folder IDs (or "__uncat__") that are collapsed - movePstId: null, // currently being moved -}; - -const $ = (id) => document.getElementById(id); - -// ---------- helpers ---------- -function fmtBytes(n) { - if (!n) return "0 B"; - const units = ["B", "KB", "MB", "GB", "TB"]; - let i = 0; - while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } - return `${n.toFixed(i ? 1 : 0)} ${units[i]}`; -} - -function fmtDate(iso) { - if (!iso) return "—"; - try { - const d = new Date(iso); - if (isNaN(d)) return iso; - return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); - } catch { return iso; } -} - -function fmtDateTime(iso) { - if (!iso) return "—"; - try { - const d = new Date(iso); - if (isNaN(d)) return iso; - return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); - } catch { return iso; } -} - -function toast(msg, ms = 3000) { - const el = $("toast"); - el.textContent = msg; - el.classList.remove("hidden"); - clearTimeout(toast._t); - toast._t = setTimeout(() => el.classList.add("hidden"), ms); -} - -function escapeHtml(s) { - if (s == null) return ""; - return String(s) - .replace(/&/g, "&").replace(//g, ">") - .replace(/"/g, """).replace(/'/g, "'"); -} - -function renderSnippet(snippet) { - if (!snippet) return ""; - return escapeHtml(snippet) - .replace(/<mark>/g, "") - .replace(/<\/mark>/g, ""); -} - -function debounce(fn, ms) { - let t; - return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; -} - -// ---------- API ---------- -async function api(path, opts = {}) { - const res = await fetch(API + path, { - credentials: "same-origin", - ...opts, - }); - let body = null; - try { body = await res.json(); } catch {} - if (!res.ok) { - const err = new Error((body && body.detail) || res.statusText); - err.status = res.status; - err.body = body || {}; - throw err; - } - return body; -} - -// ---------- Screen switching ---------- -function showScreen(screen) { - ["setupScreen", "loginScreen", "mainApp"].forEach((id) => { - $(id).classList.toggle("hidden", id !== screen); - }); -} - -// ---------- Bootstrap ---------- -async function bootstrap() { - // 1. Try to fetch current user - try { - const { user } = await api("/me"); - state.user = user; - enterApp(); - return; - } catch (e) { - if (e.status !== 401) { - showLogin(); - toast("Connection problem — please retry."); - return; - } - } - - // 2. Not logged in — check setup status - try { - const { needs_setup } = await api("/setup/status"); - if (needs_setup) showSetup(); - else showLogin(); - } catch { - showLogin(); - } -} - -// ---------- Setup ---------- -function showSetup() { - showScreen("setupScreen"); - $("setupForm").reset(); - $("setupError").classList.add("hidden"); - setTimeout(() => $("setupForm").username?.focus(), 50); -} - -async function handleSetup(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("setupError"); - errEl.classList.add("hidden"); - - const username = form.username.value.trim(); - const password = form.password.value; - const password2 = form.password2.value; - - if (password !== password2) { - errEl.textContent = "Passwords do not match."; - errEl.classList.remove("hidden"); - return; - } - - try { - const { user } = await api("/setup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - }); - state.user = user; - toast("Welcome — admin account created."); - enterApp(); - } catch (err) { - errEl.textContent = err.message || "Setup failed."; - errEl.classList.remove("hidden"); - } -} - -// ---------- Login ---------- -function showLogin() { - showScreen("loginScreen"); - $("loginForm").reset(); - $("mfaField").classList.add("hidden"); - $("loginError").classList.add("hidden"); - setTimeout(() => $("loginForm").username?.focus(), 50); -} - -async function handleLogin(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("loginError"); - errEl.classList.add("hidden"); - - const payload = { - username: form.username.value.trim(), - password: form.password.value, - }; - const mfaInput = form.totp_code.value.trim(); - if (mfaInput) payload.totp_code = mfaInput; - - try { - const { user } = await api("/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - state.user = user; - toast(`Welcome, ${user.username}.`); - enterApp(); - } catch (err) { - if (err.status === 401 && err.body && err.body.mfa_required) { - $("mfaField").classList.remove("hidden"); - const f = $("loginForm").totp_code; - f.value = ""; - setTimeout(() => f.focus(), 50); - errEl.textContent = "Enter your authenticator code to continue."; - errEl.classList.remove("hidden"); - return; - } - errEl.textContent = err.message || "Login failed."; - errEl.classList.remove("hidden"); - } -} - -async function handleLogout() { - try { await api("/logout", { method: "POST" }); } catch {} - state.user = null; - state.activePstId = null; - resetReader(); - showLogin(); -} - -// ---------- Enter the app ---------- -function enterApp() { - showScreen("mainApp"); - $("userChip").textContent = `${state.user.username}${state.user.role === "admin" ? " · admin" : ""}`; - checkHealth(); - loadPstList(); - setInterval(checkHealth, 30000); -} - -// ---------- Health ---------- -async function checkHealth() { - const dot = $("statusDot"); - const text = $("statusText"); - try { - await api("/health"); - dot.className = "status-dot ok"; - text.textContent = "connected"; - } catch { - dot.className = "status-dot err"; - text.textContent = "offline"; - } -} - -// ---------- PST list ---------- -async function loadPstList() { - try { - const [folders, items] = await Promise.all([ - api("/folders"), - api("/pst-files"), - ]); - state.folders = folders; - state.psts = items; - renderPstList(); - if (items.some((i) => i.status === "indexing")) { - clearTimeout(state.refreshTimer); - state.refreshTimer = setTimeout(loadPstList, 2500); - } - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Failed to load archives: ${e.message}`); - } -} - -function pstItemHtml(i) { - const active = i.id === state.activePstId ? "active" : ""; - const disabled = i.status !== "ready"; - const statusText = i.status === "indexing" ? "indexing…" : - i.status === "ready" ? `${i.message_count.toLocaleString()} msgs` : - i.status === "failed" ? "failed" : i.status; - return ` -
  • -
    - ${escapeHtml(i.original_name)} - -
    -
    - ${fmtBytes(i.size_bytes)} · ${fmtDate(i.uploaded_at)} - ${statusText} -
    -
  • - `; -} - -function renderPstList() { - const ul = $("pstList"); - const { folders, psts } = state; - - if (!folders.length && !psts.length) { - ul.innerHTML = '
  • No archives yet.
  • '; - return; - } - - // Group PSTs by folder_id - const byFolder = new Map(); - byFolder.set(null, []); // uncategorized bucket - folders.forEach((f) => byFolder.set(f.id, [])); - psts.forEach((p) => { - const bucket = byFolder.has(p.folder_id) ? p.folder_id : null; - byFolder.get(bucket).push(p); - }); - - const parts = []; - - // Render user-defined folders first (sorted alphabetically — server does this) - for (const f of folders) { - const items = byFolder.get(f.id) || []; - const collapsed = state.collapsedFolders.has(f.id) ? "collapsed" : ""; - parts.push(` -
  • -
    -
    - - ${escapeHtml(f.name)} - ${items.length} -
    - -
    -
      - ${items.length - ? items.map(pstItemHtml).join("") - : '
    • — empty —
    • '} -
    -
  • - `); - } - - // Uncategorized group — only shown if it has items OR there are no folders - const uncat = byFolder.get(null) || []; - if (uncat.length || !folders.length) { - const collapsed = state.collapsedFolders.has("__uncat__") ? "collapsed" : ""; - parts.push(` -
  • -
    -
    - - Uncategorized - ${uncat.length} -
    -
    -
      - ${uncat.length - ? uncat.map(pstItemHtml).join("") - : '
    • — empty —
    • '} -
    -
  • - `); - } - - ul.innerHTML = parts.join(""); - - // PST click - ul.querySelectorAll(".pst-item").forEach((el) => { - el.addEventListener("click", (e) => { - if (e.target.closest("[data-pst-menu]")) return; - if (el.dataset.disabled) { - toast(el.dataset.status === "indexing" ? "Still indexing — one moment…" : "This archive is unavailable."); - return; - } - selectPst(el.dataset.id, el.dataset.name); - }); - }); - - // PST action menu - ul.querySelectorAll("[data-pst-menu]").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - openPstMenu(e, btn.dataset.pstMenu); - }); - }); - - // Folder header toggle (collapse/expand) - ul.querySelectorAll("[data-folder-header]").forEach((el) => { - el.addEventListener("click", (e) => { - if (e.target.closest("[data-folder-menu]")) return; - const id = el.dataset.folderHeader; - if (state.collapsedFolders.has(id)) state.collapsedFolders.delete(id); - else state.collapsedFolders.add(id); - const group = el.closest(".folder-group"); - group.classList.toggle("collapsed"); - }); - }); - - // Folder action menu - ul.querySelectorAll("[data-folder-menu]").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - openFolderMenu(e, btn.dataset.folderMenu); - }); - }); -} - -// ---------- Context menus ---------- -function closeContextMenu() { - document.querySelectorAll(".context-menu").forEach((el) => el.remove()); -} - -function openContextMenu(x, y, items) { - closeContextMenu(); - const menu = document.createElement("div"); - menu.className = "context-menu"; - items.forEach((it) => { - if (it.divider) { - menu.appendChild(document.createElement("hr")); - return; - } - const btn = document.createElement("button"); - btn.textContent = it.label; - if (it.danger) btn.classList.add("danger"); - btn.addEventListener("click", () => { - closeContextMenu(); - it.onClick(); - }); - menu.appendChild(btn); - }); - document.body.appendChild(menu); - - // Position, keeping menu inside viewport - const rect = menu.getBoundingClientRect(); - const maxX = window.innerWidth - rect.width - 4; - const maxY = window.innerHeight - rect.height - 4; - menu.style.left = Math.min(x, maxX) + "px"; - menu.style.top = Math.min(y, maxY) + "px"; - - // Close on outside click or escape - setTimeout(() => { - document.addEventListener("click", closeContextMenu, { once: true }); - }, 0); -} - -function openPstMenu(event, pstId) { - const p = state.psts.find((x) => x.id === pstId); - if (!p) return; - const r = event.currentTarget.getBoundingClientRect(); - openContextMenu(r.left, r.bottom + 4, [ - { - label: "Move to folder…", - onClick: () => openMoveModal(p), - }, - { divider: true }, - { - label: "Delete archive", - danger: true, - onClick: () => deletePst(p), - }, - ]); -} - -function openFolderMenu(event, folderId) { - const f = state.folders.find((x) => x.id === folderId); - if (!f) return; - const r = event.currentTarget.getBoundingClientRect(); - openContextMenu(r.left, r.bottom + 4, [ - { - label: "Rename folder…", - onClick: () => renameFolder(f), - }, - { divider: true }, - { - label: "Delete folder", - danger: true, - onClick: () => deleteFolder(f), - }, - ]); -} - -// ---------- Folder operations ---------- -function openFolderModal() { - $("folderForm").reset(); - $("folderError").classList.add("hidden"); - $("folderModal").classList.remove("hidden"); - setTimeout(() => $("folderForm").name.focus(), 50); -} - -async function handleCreateFolder(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("folderError"); - errEl.classList.add("hidden"); - try { - await api("/folders", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: form.name.value }), - }); - toast("Folder created."); - closeModals(); - loadPstList(); - } catch (err) { - errEl.textContent = err.message || "Create failed."; - errEl.classList.remove("hidden"); - } -} - -async function renameFolder(f) { - const newName = prompt(`Rename "${f.name}" to:`, f.name); - if (!newName || newName.trim() === f.name) return; - try { - await api(`/folders/${f.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: newName }), - }); - toast("Folder renamed."); - loadPstList(); - } catch (err) { - toast(`Rename failed: ${err.message}`); - } -} - -async function deleteFolder(f) { - if (!confirm(`Delete folder "${f.name}"?\n\nAny archives inside will be moved to Uncategorized.`)) return; - try { - await api(`/folders/${f.id}`, { method: "DELETE" }); - toast("Folder deleted."); - loadPstList(); - } catch (err) { - toast(`Delete failed: ${err.message}`); - } -} - -async function deletePst(p) { - if (!confirm(`Remove "${p.original_name}" and all indexed messages?`)) return; - try { - await api(`/pst-files/${p.id}`, { method: "DELETE" }); - if (state.activePstId === p.id) { state.activePstId = null; resetReader(); } - toast("Archive removed."); - loadPstList(); - } catch (err) { - toast(`Delete failed: ${err.message}`); - } -} - -function openMoveModal(p) { - state.movePstId = p.id; - $("moveSubject").textContent = p.original_name; - - const sel = $("moveFolderSelect"); - sel.innerHTML = '' + - state.folders.map((f) => - `` - ).join(""); - if (!p.folder_id) sel.value = ""; - - $("moveError").classList.add("hidden"); - $("moveModal").classList.remove("hidden"); -} - -async function handleMovePst(e) { - e.preventDefault(); - const errEl = $("moveError"); - errEl.classList.add("hidden"); - const folderId = $("moveFolderSelect").value || null; - try { - await api(`/pst-files/${state.movePstId}/folder`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folder_id: folderId }), - }); - toast("Moved."); - closeModals(); - loadPstList(); - } catch (err) { - errEl.textContent = err.message || "Move failed."; - errEl.classList.remove("hidden"); - } -} - -function closeModals() { - document.querySelectorAll(".modal").forEach((m) => m.classList.add("hidden")); -} - -// ---------- Selection + folders ---------- -async function selectPst(id, name) { - state.activePstId = id; - state.activePstName = name; - state.folder = ""; - state.query = ""; - state.offset = 0; - - $("searchInput").value = ""; - $("searchInput").disabled = false; - $("folderFilter").disabled = false; - - document.querySelectorAll(".pst-item").forEach((el) => { - el.classList.toggle("active", el.dataset.id === id); - }); - $("readerTitle").textContent = name; - $("readerMeta").textContent = "Loading messages…"; - hideMessageView(); - await Promise.all([loadFolders(), loadMessages()]); -} - -async function loadFolders() { - if (!state.activePstId) return; - try { - const folders = await api(`/pst-files/${state.activePstId}/folders`); - const sel = $("folderFilter"); - sel.innerHTML = - '' + - folders.map((f) => ``).join(""); - sel.value = state.folder || ""; - } catch (e) { console.warn("folders", e); } -} - -// ---------- Messages ---------- -async function loadMessages() { - if (!state.activePstId) return; - const params = new URLSearchParams(); - if (state.query) params.set("q", state.query); - if (state.folder) params.set("folder", state.folder); - params.set("limit", state.limit); - params.set("offset", state.offset); - - try { - const res = await api(`/pst-files/${state.activePstId}/messages?${params}`); - state.total = res.total; - renderMessages(res.items); - renderPaginator(); - $("readerMeta").textContent = - `${state.total.toLocaleString()} message${state.total === 1 ? "" : "s"}` + - (state.query ? ` matching "${state.query}"` : "") + - (state.folder ? ` in ${state.folder}` : ""); - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Search failed: ${e.message}`); - } -} - -function renderMessages(items) { - const list = $("messageList"); - if (!items.length) { - list.innerHTML = ` -
    -
    -

    No messages match.

    -
    `; - return; - } - list.innerHTML = items.map((m) => { - const hasAtt = m.attachments && m.attachments.trim().length > 0; - return ` -
    -
    ${escapeHtml(m.sender || "(unknown sender)")}
    -
    -
    ${escapeHtml(m.subject || "(no subject)")} ${hasAtt ? '' : ""}
    -
    ${renderSnippet(m.snippet || "")}
    -
    -
    -
    ${fmtDate(m.sent_date)}
    -
    ${escapeHtml((m.folder || "").split("/").filter(Boolean).pop() || "inbox")}
    -
    -
    `; - }).join(""); - list.querySelectorAll(".message-row").forEach((el) => { - el.addEventListener("click", () => openMessage(el.dataset.id)); - }); -} - -function renderPaginator() { - const p = $("paginator"); - if (state.total <= state.limit) { p.classList.add("hidden"); return; } - p.classList.remove("hidden"); - const from = state.offset + 1; - const to = Math.min(state.offset + state.limit, state.total); - $("pageInfo").textContent = `${from}–${to} of ${state.total.toLocaleString()}`; - $("prevBtn").disabled = state.offset === 0; - $("nextBtn").disabled = state.offset + state.limit >= state.total; -} - -// ---------- Message view ---------- -async function openMessage(id) { - try { - const m = await api(`/messages/${id}`); - $("mvSubject").textContent = m.subject || "(no subject)"; - $("mvSender").textContent = m.sender || "—"; - $("mvRecipients").textContent = m.recipients || "—"; - $("mvDate").textContent = fmtDateTime(m.sent_date); - $("mvFolder").textContent = m.folder || "—"; - if (m.attachments && m.attachments.trim()) { - $("mvAttLabel").classList.remove("hidden"); - $("mvAttachments").classList.remove("hidden"); - $("mvAttachments").textContent = m.attachments.split(" | ").join(", "); - } else { - $("mvAttLabel").classList.add("hidden"); - $("mvAttachments").classList.add("hidden"); - } - $("mvBody").textContent = m.body || "(empty body)"; - $("messageView").classList.remove("hidden"); - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Could not open message: ${e.message}`); - } -} - -function hideMessageView() { $("messageView").classList.add("hidden"); } - -function resetReader() { - $("readerTitle").textContent = "Select an archive"; - $("readerMeta").textContent = "Upload a PST file or select one from the left to begin."; - $("searchInput").disabled = true; - $("folderFilter").disabled = true; - $("messageList").innerHTML = ` -
    -
    -

    The archive awaits.

    -
    `; - $("paginator").classList.add("hidden"); - hideMessageView(); -} - -// ---------- Upload ---------- -function uploadFile(file) { - if (!file) return; - const lower = file.name.toLowerCase(); - if (!lower.endsWith(".pst") && !lower.endsWith(".ost")) { - toast("Only .pst and .ost files are accepted."); - return; - } - const progress = $("uploadProgress"); - const bar = $("progressBar"); - const pct = $("progressPct"); - const nm = $("progressName"); - progress.classList.remove("hidden"); - nm.textContent = file.name; - bar.style.width = "0%"; - pct.textContent = "0%"; - - const form = new FormData(); - form.append("file", file); - - const xhr = new XMLHttpRequest(); - xhr.open("POST", API + "/pst-files"); - xhr.withCredentials = true; - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - const p = Math.round((e.loaded / e.total) * 100); - bar.style.width = p + "%"; - pct.textContent = p + "%"; - } - }; - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - pct.textContent = "indexing…"; - bar.style.width = "100%"; - toast("Upload complete. Indexing started in background."); - setTimeout(() => progress.classList.add("hidden"), 2500); - loadPstList(); - } else if (xhr.status === 401) { - handleLogout(); - } else { - let msg = "Upload failed"; - try { msg = JSON.parse(xhr.responseText).detail || msg; } catch {} - toast(msg); - progress.classList.add("hidden"); - } - }; - xhr.onerror = () => { toast("Upload failed (network)."); progress.classList.add("hidden"); }; - xhr.send(form); -} - -// ---------- Theme ---------- -function initTheme() { - // Preference order: localStorage → prefers-color-scheme → light - let theme = "light"; - try { - const saved = localStorage.getItem("pst-theme"); - if (saved === "light" || saved === "dark") { - theme = saved; - } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { - theme = "dark"; - } - } catch { /* ignore storage errors */ } - document.documentElement.dataset.theme = theme; -} - -function toggleTheme() { - const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light"; - const next = current === "dark" ? "light" : "dark"; - document.documentElement.dataset.theme = next; - try { localStorage.setItem("pst-theme", next); } catch {} -} - -// Apply theme immediately so there's no flash of wrong theme -initTheme(); - -// ---------- Wire up ---------- -function init() { - $("setupForm").addEventListener("submit", handleSetup); - $("loginForm").addEventListener("submit", handleLogin); - $("logoutBtn").addEventListener("click", handleLogout); - - // Theme toggles (both the in-masthead and floating variants) - ["themeToggle", "themeToggleFloat"].forEach((id) => { - const el = document.getElementById(id); - if (el) el.addEventListener("click", toggleTheme); - }); - - $("fileInput").addEventListener("change", (e) => { - const f = e.target.files[0]; - if (f) uploadFile(f); - e.target.value = ""; - }); - - const dz = $("dropzone"); - ["dragenter", "dragover"].forEach((ev) => { - dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("drag-over"); }); - }); - ["dragleave", "drop"].forEach((ev) => { - dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("drag-over"); }); - }); - dz.addEventListener("drop", (e) => { - const f = e.dataTransfer.files?.[0]; - if (f) uploadFile(f); - }); - - $("refreshBtn").addEventListener("click", loadPstList); - $("newFolderBtn").addEventListener("click", openFolderModal); - $("folderForm").addEventListener("submit", handleCreateFolder); - $("moveForm").addEventListener("submit", handleMovePst); - - // Modal close buttons + backdrop clicks + Escape - document.querySelectorAll("[data-close]").forEach((el) => { - el.addEventListener("click", closeModals); - }); - document.querySelectorAll(".modal").forEach((m) => { - m.addEventListener("click", (e) => { - if (e.target === m) closeModals(); - }); - }); - - const searchHandler = debounce(() => { - state.query = $("searchInput").value.trim(); - state.offset = 0; - loadMessages(); - }, 250); - $("searchInput").addEventListener("input", searchHandler); - - $("folderFilter").addEventListener("change", () => { - state.folder = $("folderFilter").value; - state.offset = 0; - loadMessages(); - }); - - $("prevBtn").addEventListener("click", () => { - state.offset = Math.max(0, state.offset - state.limit); - loadMessages(); - }); - $("nextBtn").addEventListener("click", () => { - state.offset += state.limit; - loadMessages(); - }); - - $("closeMessageBtn").addEventListener("click", hideMessageView); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - hideMessageView(); - closeModals(); - closeContextMenu(); - } - }); - - bootstrap(); -} - -document.addEventListener("DOMContentLoaded", init); diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html deleted file mode 100644 index 1e0be42..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - - -PST Archive · Mail Indexer - - - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css deleted file mode 100644 index 8aa34f3..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css +++ /dev/null @@ -1,1136 +0,0 @@ -/* ============================================================ - PST ARCHIVE — aesthetic: editorial/forensic archive - Warm paper palette, serif display, mono accents, grain overlay - Supports light (default) and dark themes via [data-theme] - ============================================================ */ - -:root, -:root[data-theme="light"] { - --paper: #f2ede3; - --paper-deep: #e8e1d3; - --ink: #1b1814; - --ink-soft: #3a332a; - --ink-muted: #6b6258; - --rule: #c9bfad; - --rule-soft: #dcd3c1; - --accent: #a8341f; /* oxblood red */ - --accent-soft: #c95a42; - --highlight: #e8d67a; /* faded yellow marker */ - --ok: #2e6f3a; - --warn: #a86b1e; - --shadow: 0 1px 0 rgba(27, 24, 20, 0.04), 0 20px 40px -20px rgba(27, 24, 20, 0.25); - --grain-opacity: 0.35; - - --font-display: "Fraunces", "Iowan Old Style", "Palatino", Georgia, serif; - --font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, monospace; -} - -:root[data-theme="dark"] { - --paper: #17140f; /* deep inked paper */ - --paper-deep: #201c16; /* slightly lighter ink-pool for surfaces */ - --ink: #e9e0cb; /* warm paper-cream as the ink color */ - --ink-soft: #c4b99f; - --ink-muted: #8a8170; - --rule: #3a3226; - --rule-soft: #2a251d; - --accent: #d06347; /* softened oxblood that holds up on dark */ - --accent-soft: #e4876c; - --highlight: #d8b64a; /* brass-yellow marker */ - --ok: #6fae7c; - --warn: #d99a4a; - --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 20px 40px -20px rgba(0, 0, 0, 0.9); - --grain-opacity: 0.12; -} - -* { box-sizing: border-box; } - -html, body { - margin: 0; - padding: 0; - height: 100%; - background: var(--paper); - color: var(--ink); - font-family: var(--font-display); - font-feature-settings: "ss01", "onum"; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - display: flex; - flex-direction: column; - min-height: 100vh; - background-image: - radial-gradient(circle at 10% 10%, rgba(168, 52, 31, 0.04), transparent 40%), - radial-gradient(circle at 90% 90%, rgba(74, 58, 42, 0.05), transparent 50%); -} - -:root[data-theme="dark"] body { - background-image: - radial-gradient(circle at 10% 10%, rgba(208, 99, 71, 0.06), transparent 40%), - radial-gradient(circle at 90% 90%, rgba(216, 182, 74, 0.03), transparent 50%); -} - -/* grain overlay */ -.grain { - pointer-events: none; - position: fixed; - inset: 0; - z-index: 9999; - opacity: var(--grain-opacity); - mix-blend-mode: multiply; - background-image: url("data:image/svg+xml;utf8,"); -} - -:root[data-theme="dark"] .grain { - mix-blend-mode: screen; -} - -/* ============================================================ - MASTHEAD - ============================================================ */ -.masthead { - display: flex; - justify-content: space-between; - align-items: flex-end; - padding: 28px 40px 18px; - border-bottom: 1px solid var(--ink); - position: relative; -} - -.masthead::after { - content: ""; - position: absolute; - bottom: -4px; - left: 40px; - right: 40px; - height: 1px; - background: var(--ink); -} - -.masthead-left { display: flex; align-items: center; gap: 18px; } - -.logo-mark { - color: var(--ink); - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border: 1px solid var(--ink); - background: var(--paper); -} - -.masthead-title h1 { - margin: 0; - font-size: 2.5rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1; - font-variation-settings: "SOFT" 30, "WONK" 1; -} - -.tagline { - margin: 6px 0 0; - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.masthead-right { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--warn); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); -} -.status-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); } -.status-dot.err { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); } - -/* ============================================================ - WORKSPACE - ============================================================ */ -.workspace { - display: grid; - grid-template-columns: 340px 1fr; - flex: 1; - min-height: 0; -} - -/* ============================================================ - SIDEBAR - ============================================================ */ -.sidebar { - border-right: 1px solid var(--rule); - padding: 28px 24px; - display: flex; - flex-direction: column; - gap: 36px; - overflow-y: auto; - background: linear-gradient(180deg, rgba(232, 225, 211, 0.4), transparent 40%); -} - -.section-heading { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--ink-soft); - margin: 0 0 14px; - padding-bottom: 8px; - border-bottom: 1px solid var(--rule); - display: flex; - align-items: baseline; - justify-content: space-between; -} - -.dropzone { - display: block; - border: 1.5px dashed var(--ink-muted); - background: var(--paper-deep); - padding: 28px 18px; - text-align: center; - cursor: pointer; - transition: all 0.2s ease; - position: relative; -} -.dropzone:hover, -.dropzone.drag-over { - border-color: var(--accent); - background: var(--rule-soft); - transform: translateY(-1px); -} -.dropzone-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - color: var(--ink-soft); -} -.dropzone-title { - font-family: var(--font-display); - font-size: 1.1rem; - font-style: italic; - font-weight: 400; -} -.dropzone-title em { - font-style: normal; - font-family: var(--font-mono); - font-size: 0.9rem; - color: var(--accent); -} -.dropzone-hint { - font-family: var(--font-mono); - font-size: 0.65rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.upload-progress { margin-top: 14px; } -.progress-label { - display: flex; - justify-content: space-between; - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--ink-muted); - margin-bottom: 6px; -} -.progress-track { - height: 4px; - background: var(--rule-soft); - overflow: hidden; -} -.progress-bar { - height: 100%; - width: 0%; - background: var(--accent); - transition: width 0.2s ease; -} - -.archive-head { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 14px; -} -.archive-head .section-heading { - margin: 0; - border: none; - padding: 0; -} - -.icon-btn { - background: transparent; - border: 1px solid var(--rule); - padding: 5px 8px; - cursor: pointer; - color: var(--ink-soft); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} -.icon-btn:hover { - border-color: var(--ink); - color: var(--accent); -} - -.pst-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; } - -.pst-item { - padding: 14px 14px; - border: 1px solid var(--rule); - background: var(--paper); - cursor: pointer; - transition: all 0.15s ease; - position: relative; -} -.pst-item:hover { - border-color: var(--ink); - transform: translateX(2px); -} -.pst-item.active { - border-color: var(--ink); - background: var(--ink); - color: var(--paper); - box-shadow: 4px 4px 0 var(--accent); -} -.pst-item.active .pst-meta { color: color-mix(in srgb, var(--paper) 65%, transparent); } - -.pst-name { - font-size: 1rem; - font-weight: 500; - letter-spacing: -0.01em; - margin: 0 0 6px; - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 8px; -} -.pst-name-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} -.pst-menu { - background: none; - border: none; - color: inherit; - opacity: 0.4; - cursor: pointer; - font-size: 1.1rem; - line-height: 1; - padding: 0 4px; - letter-spacing: 0.1em; - transition: opacity 0.15s, color 0.15s; -} -.pst-menu:hover { opacity: 1; color: var(--accent); } -.pst-item.active .pst-menu:hover { color: var(--accent-soft); } - -.pst-meta { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.08em; - color: var(--ink-muted); - display: flex; - justify-content: space-between; - gap: 8px; -} - -.pst-status { - text-transform: uppercase; - letter-spacing: 0.12em; -} -.pst-status.indexing { color: var(--warn); } -.pst-status.ready { color: var(--ok); } -.pst-status.failed { color: var(--accent); } -.pst-item.active .pst-status.indexing { color: color-mix(in srgb, var(--warn) 70%, var(--paper) 30%); } -.pst-item.active .pst-status.ready { color: color-mix(in srgb, var(--ok) 70%, var(--paper) 30%); } -.pst-item.active .pst-status.failed { color: var(--accent-soft); } - -.empty-state { - color: var(--ink-muted); - font-style: italic; - text-align: center; - padding: 20px 0; - font-size: 0.9rem; -} - -/* ============================================================ - FOLDER GROUPS IN SIDEBAR - ============================================================ */ -.archive-actions { - display: flex; - gap: 4px; -} - -.folder-group { - list-style: none; - padding: 0; - margin: 0; -} - -.folder-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 4px; - margin: 14px 0 6px; - cursor: pointer; - user-select: none; - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-soft); - border-bottom: 1px solid var(--rule); - transition: color 0.15s; -} -.folder-group:first-child .folder-header { margin-top: 4px; } - -.folder-header:hover { color: var(--accent); } - -.folder-header-left { - display: flex; - align-items: center; - gap: 6px; - overflow: hidden; -} - -.folder-caret { - display: inline-block; - width: 8px; - transition: transform 0.2s; - color: var(--ink-muted); -} -.folder-group.collapsed .folder-caret { transform: rotate(-90deg); } - -.folder-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.folder-count { - color: var(--ink-muted); - font-size: 0.65rem; - margin-left: 4px; -} - -.folder-menu-btn { - background: none; - border: none; - color: var(--ink-muted); - cursor: pointer; - padding: 2px 6px; - font-size: 0.9rem; - line-height: 1; - opacity: 0; - transition: opacity 0.15s, color 0.15s; -} -.folder-header:hover .folder-menu-btn { opacity: 1; } -.folder-menu-btn:hover { color: var(--accent); } - -.folder-items { - display: flex; - flex-direction: column; - gap: 8px; - overflow: hidden; - transition: max-height 0.25s ease-out; - max-height: 2000px; -} -.folder-group.collapsed .folder-items { - max-height: 0; -} - -.folder-empty { - font-style: italic; - font-size: 0.8rem; - color: var(--ink-muted); - padding: 6px 4px; -} - -/* Context menu for folder/PST actions */ -.context-menu { - position: fixed; - background: var(--paper); - border: 1px solid var(--ink); - box-shadow: 4px 4px 0 var(--rule); - z-index: 500; - min-width: 160px; - padding: 4px 0; -} -.context-menu button { - display: block; - width: 100%; - text-align: left; - background: none; - border: none; - padding: 8px 14px; - font-family: var(--font-display); - font-size: 0.9rem; - color: var(--ink); - cursor: pointer; -} -.context-menu button:hover { - background: var(--paper-deep); - color: var(--accent); -} -.context-menu button.danger:hover { - color: var(--accent); -} -.context-menu hr { - border: none; - border-top: 1px solid var(--rule-soft); - margin: 4px 0; -} - -/* ============================================================ - MODAL - ============================================================ */ -.modal { - position: fixed; - inset: 0; - background: color-mix(in srgb, var(--ink) 40%, transparent); - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - z-index: 200; - animation: fadeIn 0.2s ease-out; -} - -.modal-card { - background: var(--paper); - border: 1px solid var(--ink); - padding: 36px 36px 30px; - max-width: 420px; - width: 100%; - box-shadow: 8px 8px 0 var(--accent); - position: relative; -} -.modal-card h2 { - margin: 0 0 4px; - font-size: 1.5rem; - font-weight: 500; - letter-spacing: -0.015em; -} -.modal-subtitle { - margin: 0 0 24px; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--ink-muted); - letter-spacing: 0.06em; -} -.modal-close { - position: absolute; - top: 10px; - right: 14px; - background: none; - border: none; - font-size: 1.8rem; - line-height: 1; - cursor: pointer; - color: var(--ink-muted); - transition: color 0.15s, transform 0.15s; -} -.modal-close:hover { - color: var(--accent); - transform: rotate(90deg); -} - -.auth-label select { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 12px; - border: 1px solid var(--ink); - background: var(--paper-deep); - color: var(--ink); - outline: none; - cursor: pointer; -} -.auth-label select:focus { - background: var(--paper); - box-shadow: 3px 3px 0 var(--accent); -} - -/* ============================================================ - READER - ============================================================ */ -.reader { - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; -} - -.reader-head { - padding: 28px 40px 20px; - border-bottom: 1px solid var(--rule); - background: - linear-gradient(180deg, rgba(232, 225, 211, 0.3), transparent); -} - -.reader-title h2 { - margin: 0; - font-size: 2rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1.1; -} - -.reader-meta { - margin: 6px 0 20px; - font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.search-wrap { - display: grid; - grid-template-columns: 1fr 200px; - gap: 10px; -} - -#searchInput, #folderFilter { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 14px; - border: 1px solid var(--ink); - background: var(--paper); - color: var(--ink); - outline: none; - transition: box-shadow 0.15s; -} -#searchInput:focus, #folderFilter:focus { - box-shadow: 3px 3px 0 var(--accent); -} -#searchInput:disabled, #folderFilter:disabled { - border-color: var(--rule); - color: var(--ink-muted); - background: var(--paper-deep); - cursor: not-allowed; -} -#searchInput::placeholder { - color: var(--ink-muted); - font-style: italic; -} -#folderFilter { cursor: pointer; font-family: var(--font-mono); font-size: 0.8rem; } - -.reader-body { - flex: 1; - overflow-y: auto; - padding: 20px 40px; - position: relative; -} - -.reader-placeholder { - text-align: center; - padding: 80px 20px; - color: var(--ink-muted); -} -.placeholder-ornament { - font-size: 2rem; - color: var(--accent); - margin-bottom: 16px; -} -.reader-placeholder p { - font-style: italic; - font-size: 1.1rem; - margin: 0; -} - -/* ============================================================ - MESSAGE LIST - ============================================================ */ -.message-list { - display: flex; - flex-direction: column; -} - -.message-row { - padding: 16px 0; - border-bottom: 1px solid var(--rule-soft); - cursor: pointer; - transition: all 0.12s ease; - display: grid; - grid-template-columns: 180px 1fr 140px; - gap: 20px; - align-items: baseline; -} -.message-row:hover { - background: color-mix(in srgb, var(--paper-deep) 70%, transparent); - padding-left: 8px; - padding-right: 8px; -} - -.mr-sender { - font-weight: 500; - color: var(--ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.95rem; -} - -.mr-main { - min-width: 0; - overflow: hidden; -} -.mr-subject { - font-size: 1.05rem; - font-weight: 500; - margin: 0 0 4px; - color: var(--ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.mr-snippet { - font-size: 0.85rem; - color: var(--ink-muted); - line-height: 1.5; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; -} -.mr-snippet mark { - background: var(--highlight); - color: var(--ink); - padding: 0 2px; - font-weight: 500; -} - -.mr-meta { - text-align: right; - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--ink-muted); - letter-spacing: 0.04em; -} -.mr-folder { - margin-top: 4px; - font-size: 0.65rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--accent); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mr-attached { - display: inline-block; - margin-left: 4px; - color: var(--accent); -} - -/* ============================================================ - MESSAGE VIEW - ============================================================ */ -.message-view { - position: absolute; - inset: 20px 40px; - background: var(--paper); - border: 1px solid var(--ink); - box-shadow: 8px 8px 0 var(--rule); - overflow-y: auto; - padding: 40px 50px; - animation: slideIn 0.25s ease-out; -} - -@keyframes slideIn { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - -.close-btn { - position: absolute; - top: 14px; - right: 18px; - background: none; - border: none; - font-size: 2rem; - line-height: 1; - cursor: pointer; - color: var(--ink-muted); - transition: color 0.15s, transform 0.15s; -} -.close-btn:hover { - color: var(--accent); - transform: rotate(90deg); -} - -.message-header { - border-bottom: 2px solid var(--ink); - padding-bottom: 20px; - margin-bottom: 24px; -} - -.message-subject { - font-size: 1.75rem; - font-weight: 500; - line-height: 1.2; - letter-spacing: -0.015em; - margin-bottom: 16px; -} - -.message-meta { - display: grid; - grid-template-columns: 80px 1fr; - gap: 4px 20px; - margin: 0; -} -.message-meta dt { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); - padding-top: 2px; -} -.message-meta dd { - margin: 0; - font-size: 0.95rem; - color: var(--ink-soft); - word-break: break-word; -} - -.message-body { - font-family: var(--font-display); - font-size: 1rem; - line-height: 1.7; - color: var(--ink); - white-space: pre-wrap; - word-wrap: break-word; - max-width: 70ch; -} - -/* ============================================================ - PAGINATOR - ============================================================ */ -.paginator { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 40px; - border-top: 1px solid var(--rule); - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--ink-soft); - background: var(--paper-deep); -} -.paginator button { - background: none; - border: 1px solid var(--ink); - padding: 6px 14px; - cursor: pointer; - font-family: inherit; - font-size: inherit; - color: var(--ink); - transition: all 0.15s; -} -.paginator button:hover:not(:disabled) { - background: var(--ink); - color: var(--paper); -} -.paginator button:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -/* ============================================================ - TOAST - ============================================================ */ -.toast { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: var(--ink); - color: var(--paper); - padding: 12px 20px; - font-family: var(--font-mono); - font-size: 0.8rem; - letter-spacing: 0.06em; - border-left: 3px solid var(--accent); - box-shadow: var(--shadow); - z-index: 1000; - animation: toastSlide 0.3s ease-out; -} -@keyframes toastSlide { - from { opacity: 0; transform: translate(-50%, 20px); } - to { opacity: 1; transform: translate(-50%, 0); } -} - -.hidden { display: none !important; } - -/* ============================================================ - AUTH SCREENS (login + setup) - ============================================================ */ -.auth-screen { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - background: - radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 45%), - radial-gradient(circle at 70% 80%, color-mix(in srgb, var(--ink) 10%, transparent), transparent 55%), - var(--paper); - z-index: 100; - animation: fadeIn 0.3s ease-out; -} -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.auth-card { - width: 100%; - max-width: 440px; - background: var(--paper); - border: 1px solid var(--ink); - padding: 44px 40px 36px; - box-shadow: 8px 8px 0 var(--rule); - position: relative; -} -.auth-card::before { - content: ""; - position: absolute; - top: -6px; - left: -6px; - right: 6px; - bottom: 6px; - border: 1px solid var(--ink); - pointer-events: none; - z-index: -1; - background: var(--paper-deep); -} - -.auth-brand { - text-align: center; - margin-bottom: 28px; - padding-bottom: 20px; - border-bottom: 1px solid var(--rule); -} -.auth-brand svg { color: var(--ink); margin: 0 auto 12px; display: block; } -.auth-brand h1 { - margin: 0; - font-size: 2rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1; -} -.auth-tagline { - margin: 8px 0 0; - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.auth-form { display: flex; flex-direction: column; gap: 16px; } - -.auth-intro { - margin: 0 0 6px; - font-size: 0.92rem; - color: var(--ink-soft); - line-height: 1.5; - font-style: italic; -} - -.auth-label { - display: flex; - flex-direction: column; - gap: 6px; -} -.auth-label > span { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--ink-soft); -} -.auth-label small { - font-family: var(--font-mono); - font-size: 0.66rem; - color: var(--ink-muted); - letter-spacing: 0.04em; -} -.auth-label input { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 12px; - border: 1px solid var(--ink); - background: var(--paper-deep); - color: var(--ink); - outline: none; - transition: box-shadow 0.15s, background 0.15s; -} -.auth-label input:focus { - background: var(--paper); - box-shadow: 3px 3px 0 var(--accent); -} - -.auth-btn { - margin-top: 8px; - padding: 12px 18px; - font-family: var(--font-display); - font-size: 1.05rem; - font-weight: 500; - letter-spacing: -0.01em; - background: var(--ink); - color: var(--paper); - border: 1px solid var(--ink); - cursor: pointer; - transition: all 0.15s; -} -.auth-btn:hover { - background: var(--accent); - border-color: var(--accent); - transform: translate(-1px, -1px); - box-shadow: 3px 3px 0 var(--ink); -} - -.auth-error { - padding: 10px 14px; - background: color-mix(in srgb, var(--accent) 10%, transparent); - border-left: 3px solid var(--accent); - color: var(--accent); - font-size: 0.88rem; - font-family: var(--font-mono); - letter-spacing: 0.02em; -} - -/* ============================================================ - USER CHIP in masthead - ============================================================ */ -.user-chip { - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ink-soft); - padding: 4px 10px; - border: 1px solid var(--rule); - background: var(--paper-deep); - margin-left: 12px; -} - -.logout-btn { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-soft); - background: transparent; - border: 1px solid var(--rule); - padding: 5px 10px; - cursor: pointer; - transition: all 0.15s; - margin-left: 8px; -} -.logout-btn:hover { - border-color: var(--accent); - color: var(--accent); -} - -/* ============================================================ - THEME TOGGLE - ============================================================ */ -.theme-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--rule); - background: var(--paper-deep); - color: var(--ink-soft); - cursor: pointer; - padding: 0; - transition: all 0.2s ease; - position: relative; - overflow: hidden; -} -.theme-toggle:hover { - border-color: var(--accent); - color: var(--accent); - transform: translateY(-1px); -} -.theme-toggle svg { - width: 16px; - height: 16px; - transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.theme-toggle:hover svg { - transform: rotate(25deg); -} -:root[data-theme="dark"] .theme-toggle:hover svg { - transform: rotate(-25deg); -} - -/* Icon visibility — show sun in light mode, moon in dark mode */ -.theme-toggle .icon-sun { display: inline-block; } -.theme-toggle .icon-moon { display: none; } -:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; } -:root[data-theme="dark"] .theme-toggle .icon-moon { display: inline-block; } - -/* Floating variant shown on auth/setup screens */ -.theme-toggle-float { - position: fixed; - top: 20px; - right: 20px; - z-index: 200; -} -/* Hide the floating toggle once the main app is visible (in-masthead toggle takes over) */ -body:has(#mainApp:not(.hidden)) .theme-toggle-float { display: none; } - -/* ============================================================ - SCROLLBARS - ============================================================ */ -::-webkit-scrollbar { width: 10px; height: 10px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--rule); - border: 2px solid var(--paper); -} -::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); } - -/* ============================================================ - RESPONSIVE - ============================================================ */ -@media (max-width: 900px) { - .workspace { grid-template-columns: 1fr; } - .sidebar { border-right: none; border-bottom: 1px solid var(--rule); max-height: 50vh; } - .masthead { padding: 20px 24px 14px; flex-wrap: wrap; gap: 16px; } - .reader-head, .reader-body, .paginator { padding-left: 24px; padding-right: 24px; } - .search-wrap { grid-template-columns: 1fr; } - .message-row { grid-template-columns: 1fr; gap: 6px; } - .mr-meta { text-align: left; } - .message-view { inset: 12px; padding: 24px 20px; } - .message-meta { grid-template-columns: 60px 1fr; } -} diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.1 b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.1 deleted file mode 160000 index a95f71d..0000000 --- a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a95f71dc2a9a621e6bf24a525b27d6c6833d6b3a diff --git a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.2/pst-indexer.zip b/Forensic-Mail-Indexer/OLD_VERSIONS/v2.2/pst-indexer.zip deleted file mode 100644 index 674bbdd..0000000 Binary files a/Forensic-Mail-Indexer/OLD_VERSIONS/v2.2/pst-indexer.zip and /dev/null differ diff --git a/Forensic-Mail-Indexer/README.md b/Forensic-Mail-Indexer/README.md deleted file mode 100644 index 842fd15..0000000 --- a/Forensic-Mail-Indexer/README.md +++ /dev/null @@ -1,1172 +0,0 @@ -# PST Archive — Mail Indexer - -A self-hosted, Dockerized application for **uploading Microsoft Outlook PST/OST files**, **indexing every email inside them**, and **searching the contents through a web browser** — with full **user authentication**, **multi-factor authentication (MFA)**, **encrypted-at-rest credentials**, and a **built-in administration section** for admin users. - -Designed for **forensic review, e-discovery, personal archive browsing, and migration audits** — any situation where you need to crack open a PST and actually find things inside it without loading it into Outlook. - ---- - -## What's new in v2.2 - -- **Administration now runs inside the main application** — there is no longer a - separate admin process or admin port (previously the panel ran on its own - port, e.g. mapped to `9001`). Everything is served on the single app port - (`8000` by default). -- **"Administration" button** — admin users see an **ADMINISTRATION** button in - the top-right of the masthead, next to their username. It opens the - administration section at `/admin`. The button is not shown to non-admin - users. -- **Access is restricted to admins** — the administration UI and every - `/api/admin/*` endpoint require an authenticated session **and** the `admin` - role. Non-admins receive `403`; unauthenticated requests receive `401`. -- **Footer** — every in-app page (the archive view and the administration view) - now shows `© 2026 Martinhal IT - Joao Vaz - Version 2.2`. The login/setup - screen has no footer. -- **Updating is drop-in** — extract this release over your existing install and - rebuild; see [Updating to a new version](#updating-to-a-new-version). Your - `./data` directory (database, uploaded PSTs, encryption key) is never touched. - -> Historical note: some deep-dive sections below still describe the original -> two-port design (main app + separate admin panel) for reference. Operationally, -> as of v2.2 both are the same process on one port, with administration at -> `/admin`. - ---- - -## Table of Contents - -1. [Features](#features) -2. [Quick Start](#quick-start) -3. [First-Run Setup](#first-run-setup) -4. [Running as a Daemon](#running-as-a-daemon) -5. [Theming & Dark Mode](#theming--dark-mode) -6. [Authentication & Security Model](#authentication--security-model) -7. [Admin Panel](#admin-panel) -8. [Organizing PSTs with Folders](#organizing-psts-with-folders) -9. [Folder Access Permissions](#folder-access-permissions) -10. [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa) -11. [Encryption at Rest](#encryption-at-rest) -12. [How It Works](#how-it-works) -13. [Architecture](#architecture) -14. [Project Layout](#project-layout) -15. [Data Model](#data-model) -16. [REST API Reference](#rest-api-reference) -17. [Frontend Interfaces](#frontend-interfaces) -18. [Search Syntax](#search-syntax) -19. [Configuration](#configuration) -20. [Persistence & Backup](#persistence--backup) -21. [Troubleshooting](#troubleshooting) -22. [Performance Notes](#performance-notes) -23. [Security Considerations](#security-considerations) -24. [Extending the App](#extending-the-app) - ---- - -## Features - -### Core -- **Drag-and-drop upload** of `.pst` and `.ost` files through the browser -- **Streaming upload with progress bar** — handles multi-gigabyte archives -- **Background indexing** into SQLite with **FTS5 full-text search** (ranked, prefix-matched, with highlighted snippets) -- **Multi-archive support**: switch between uploaded PSTs; each is searched independently -- **Folder organization**: create folders in the sidebar to group related PST files (e.g. by client, case, year). Move archives between folders, rename folders, or delete folders (any archives inside are safely moved to Uncategorized) -- **Per-folder access permissions**: admins can restrict which non-admin users can view each folder. A single user can be granted access to many folders; a single folder can be shared with many users (true many-to-many) -- **Folder filter**: narrow search to any folder within the archive (Inbox, Sent Items, nested folders) -- **Full message viewer**: subject, sender, recipients, date, folder, attachments, body -- **Persistent storage**: everything lives in a mounted `./data` volume - -### Security -- **First-run setup wizard** forces creation of an initial administrator before any other action is possible -- **Password authentication** using **Argon2id** (winner of the Password Hashing Competition) with constant-time verification -- **TOTP-based MFA** (RFC 6238; compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator, and every other standard authenticator app) -- **Encryption at rest** for all user PII — usernames, password hashes, and TOTP secrets are stored as Fernet (AES-128-CBC + HMAC-SHA256) ciphertexts -- **Master key** auto-generated on first run and stored with `chmod 600`, or overridable via `MASTER_SECRET` environment variable -- **HMAC-based username lookup** — usernames are never stored in plaintext but are still indexable for login -- **JWT session cookies**, signed with an HKDF-derived sub-key, HttpOnly, SameSite=Lax -- **Rate limiting** on login attempts (10 failures per 5 minutes per IP+username) -- **Admin-only user management**: create, delete, role-change, password-reset, MFA-enroll other users -- **Admin file & folder management**: system-wide view of every uploaded PST and every folder, with create/rename/delete for folders and force-delete for any file regardless of uploader -- **Built-in administration section** for admins (at `/admin`) — restricted to the `admin` role, and lockable at the path level behind a reverse proxy/VPN - -### UI -- **Light and dark themes** on both the main app and the admin panel, with a sun/moon toggle in the masthead and on login/setup screens -- Preference persists in `localStorage` per-browser and respects the operating system's `prefers-color-scheme` on first visit -- Theme applied inline in `` before stylesheets load — **no flash of wrong theme** on page load -- Dark mode uses a custom "inked paper" palette (deep charcoal with warm paper-cream text and softened oxblood accent) — intentional character, not generic-dark -- Editorial typography (Fraunces serif + JetBrains Mono), grain overlay, responsive layout down to mobile widths - ---- - -## Quick Start - -### Prerequisites - -- Docker 20.10 or newer -- Docker Compose v2 -- A `.pst` or `.ost` file to index - -### Run (as a daemon — recommended) - -```bash -docker compose up -d --build -``` - -The `-d` flag runs the container in the background (detached). On the first run this takes **3–5 minutes** because `libpff-python` compiles a C extension from source; subsequent startups are nearly instant. - -The container is configured with `restart: unless-stopped`, so it will automatically come back up after a reboot or a crash, and keep running across Docker daemon restarts — until you explicitly stop it. - -Once the container is up: - -- **App**: `http://localhost:8000` -- **Administration**: `http://localhost:8000/admin` — or just click the - **ADMINISTRATION** button in the top-right after signing in as an admin. - (Non-admin users do not see the button and cannot reach `/admin`.) - -### Foreground mode (useful for debugging) - -```bash -docker compose up --build -``` - -This runs the container attached to your terminal so you can see logs inline. Press `Ctrl-C` to stop. - -### Tail the logs - -```bash -docker compose logs -f # follow both streams -docker compose logs -f pst-indexer # explicit service name -``` - -### Check status - -```bash -docker compose ps # is it running? -docker compose top # which processes? -``` - -### Stop - -```bash -docker compose stop # stop, keep data, keep container -docker compose down # stop and remove the container, keep data volume -``` - -### Full reset - -```bash -docker compose down -rm -rf ./data # erases everything including users and the master key -docker compose up -d --build -``` - -### Restart / update - -```bash -# After editing code or pulling new images: -docker compose up -d --build # rebuild & restart with one command -# Or just restart without rebuilding: -docker compose restart -``` - ---- - -## First-Run Setup - -On the very first visit to `http://localhost:8000`, you will be presented with a **setup wizard** that asks you to create the initial administrator: - -1. Choose a username (3–64 characters) -2. Choose a password (8+ characters — use a passphrase you'll remember) -3. Confirm the password - -The setup endpoint is only callable **while the users table is empty**. Once the first admin is created, the endpoint returns `409 Conflict` and is effectively disabled. - -After setup, you are automatically signed in and landed on the main app. - -**Heads up:** the very first account created through the setup wizard is an **administrator**. After that, sign in normally and use the **ADMINISTRATION** button (top-right) to manage users, folders, files, and permissions. The administration section and its API are only accessible to admin users. - ---- - -## Running as a Daemon - -By default, this app is designed to **run as a long-lived background service**. The `docker-compose.yml` sets `restart: unless-stopped`, which means once started, the container will: - -- Keep running after you close your terminal -- Automatically restart if the process crashes -- Automatically restart when the host reboots and Docker comes back up -- **Not** restart if you explicitly stop it with `docker compose stop` (hence "unless-stopped") - -### Starting the daemon - -```bash -docker compose up -d --build -``` - -The `-d` flag is short for "detached" — the container is launched in the background and the command returns immediately. Your terminal is free to do other things; the container keeps running. - -### Verifying it's running - -```bash -docker compose ps -``` - -You should see output like: - -``` -NAME IMAGE STATUS PORTS -pst-indexer pst-indexer-pst-indexer Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp -``` - -The `(healthy)` flag comes from the built-in healthcheck, which probes both `/api/health` endpoints every 30 seconds. - -### Watching logs - -Since the container is detached, you won't see logs in your terminal by default. Attach to them on demand: - -```bash -docker compose logs # print recent logs and exit -docker compose logs -f # follow (live tail), Ctrl-C to detach -docker compose logs -f --tail=100 # follow, starting from the last 100 lines -docker compose logs --since 1h # only logs from the last hour -``` - -Detaching from `-f` does **not** stop the container — it just stops showing you the logs. - -### Stopping - -```bash -docker compose stop # stop the container but keep it around -docker compose start # start it again if previously stopped -docker compose restart # stop and start in one command -docker compose down # stop and remove the container (data volume is kept) -docker compose down -v # stop, remove container, AND remove anonymous volumes -``` - -None of these delete your `./data` directory — that's a bind mount, not a Docker-managed volume, so it's always yours. - -### Survives reboots - -Because `restart: unless-stopped` is set, after a host reboot Docker will automatically start the container as soon as the Docker daemon comes up. Nothing manual required on your part. - -If you ever explicitly stopped it with `docker compose stop`, Docker will *not* auto-start it after reboot (that's the whole point of "unless-stopped"). Run `docker compose start` or `docker compose up -d` to bring it back. - -### Updating to a new version - -This release is a **drop-in update**: extract the archive over your existing -installation directory (the one containing `docker-compose.yml` and your -`data/` folder) and rebuild. The archive contains only application code — it -has **no `data/` directory**, so your database, uploaded PSTs, and encryption -key are never overwritten. - -```bash -# From the parent directory that holds your existing install folder: -# (the folder with docker-compose.yml and data/ inside it) -7z x pst-archive-v2.2.7z # extract, overwriting code files in place - -# Then rebuild and recreate the container: -cd -docker compose up -d --build -``` - -Docker Compose compares the current image to the built one; if they differ, it rebuilds and recreates the container with zero-downtime-style replacement. Your `./data` volume is preserved throughout. - -**Notes for this update (v2.2):** - -- The separate admin port is gone. If your `docker-compose.yml` previously - published a second port for the admin panel (for example `"9001:8001"` or - `"8001:8001"`), that line has been removed in the shipped compose file. If you - maintain a customized compose file, simply delete the admin-port line — the - administration section is now served on the main port at `/admin`. -- No database migration is required. The schema is unchanged, so all existing - users, folders, files, permissions, and MFA enrollments carry over as-is. -- A rebuild (`--build`) is required so the updated backend and frontend are - baked into the image. - ---- - -## Theming & Dark Mode - -Both the main app and the admin panel support **light and dark themes**, with a sun/moon toggle visible in the masthead (when signed in) and floating in the top-right corner on the login/setup screens. - -### How the toggle works - -- Click the icon to flip between themes -- Choice is saved to `localStorage["pst-theme"]` in your browser -- On a fresh browser (nothing saved), the app respects your operating system's `prefers-color-scheme` setting -- The theme is applied by an inline ` - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Forensic-Mail-Indexer/backend/admin_app.py b/Forensic-Mail-Indexer/backend/admin_app.py deleted file mode 100644 index 917217b..0000000 --- a/Forensic-Mail-Indexer/backend/admin_app.py +++ /dev/null @@ -1,597 +0,0 @@ -""" -PST Indexer — Admin Panel (DEPRECATED / no longer launched). - -As of v2.2 the administration section runs inside the main application -(app.py), served at /admin and reachable via the "Administration" button that -is shown to admin users. The entrypoint no longer starts this module, and no -separate admin port is exposed. This file is retained only for reference and -for backwards compatibility with any custom tooling that imported it; it is -safe to ignore. See app.py for the live administration endpoints -(namespaced under /api/admin). - -Original description below. - -Separate FastAPI instance on its own port so operators can place it behind a -more restrictive firewall rule or VPN than the main app. - -Exposes: -- Admin login (separate session cookie, scoped "admin") -- List, create, delete users -- Toggle admin role -- Reset passwords -- Enroll / confirm / disable MFA for any user (admin-managed flow) -- Self-service MFA for the currently logged-in admin -""" -import io -import os -from pathlib import Path -from typing import Optional - -import qrcode -from fastapi import ( - Depends, FastAPI, HTTPException, Request, Response, status, -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse, JSONResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - -import auth -from auth import ( - ADMIN_SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, - User, begin_mfa_enrollment, confirm_mfa_enrollment, count_admins, - count_users, create_session_token, create_user, delete_user, - disable_mfa, find_user_by_id, find_user_by_username, - get_totp_secret, list_users, login_limiter, record_login, - require_admin_user, set_user_role, totp_provisioning_uri, - update_password, verify_password, verify_totp, -) - -ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) -COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" - -app = FastAPI(title="PST Indexer Admin Panel") -app.add_middleware( - CORSMiddleware, - allow_origins=[], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -def _startup(): - auth.init_auth_schema() - - -def _set_admin_cookie(response: Response, token: str): - response.set_cookie( - key=ADMIN_SESSION_COOKIE_NAME, - value=token, - max_age=SESSION_TTL_SECONDS, - httponly=True, - secure=COOKIE_SECURE, - samesite="lax", - path="/", - ) - - -def _clear_admin_cookie(response: Response): - response.delete_cookie(key=ADMIN_SESSION_COOKIE_NAME, path="/") - - -# --------------------------------------------------------------------------- -# Auth -# --------------------------------------------------------------------------- -class LoginPayload(BaseModel): - username: str - password: str - totp_code: Optional[str] = None - - -@app.get("/api/health") -def health(): - return {"status": "ok", "admins": count_admins(), "users": count_users()} - - -@app.get("/api/setup/status") -def setup_status(): - """True if no users exist. Admin UI redirects to main app to complete setup.""" - return {"needs_setup": count_users() == 0} - - -@app.post("/api/login") -def admin_login(payload: LoginPayload, request: Request, response: Response): - ip = request.client.host if request.client else "unknown" - rate_key = f"admin:{ip}:{payload.username.lower()}" - if not login_limiter.check(rate_key): - raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") - - found = find_user_by_username(payload.username) - if not found: - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - user, pw_hash = found - - if not verify_password(pw_hash, payload.password): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - if not user.is_admin: - raise HTTPException(403, "Admin access required") - - if user.mfa_enabled: - if not payload.totp_code: - return JSONResponse( - status_code=401, - content={"detail": "MFA required", "mfa_required": True}, - ) - secret = get_totp_secret(user.id) - if not secret or not verify_totp(secret, payload.totp_code): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid MFA code") - - login_limiter.reset(rate_key) - record_login(user.id) - token = create_session_token(user, scope="admin") - _set_admin_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/logout") -def admin_logout(response: Response): - _clear_admin_cookie(response) - return {"ok": True} - - -@app.get("/api/me") -def me(user: User = Depends(require_admin_user)): - return {"user": user.public_dict()} - - -# --------------------------------------------------------------------------- -# User management -# --------------------------------------------------------------------------- -class CreateUserPayload(BaseModel): - username: str - password: str - role: str = "user" - - -class UpdatePasswordPayload(BaseModel): - password: str - - -class UpdateRolePayload(BaseModel): - role: str - - -@app.get("/api/users") -def api_list_users(_: User = Depends(require_admin_user)): - return [u.public_dict() for u in list_users()] - - -@app.post("/api/users") -def api_create_user( - payload: CreateUserPayload, - admin: User = Depends(require_admin_user), -): - try: - user = create_user(payload.username, payload.password, role=payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - return user.public_dict() - - -@app.delete("/api/users/{user_id}") -def api_delete_user(user_id: str, admin: User = Depends(require_admin_user)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - # Prevent deleting yourself - if target.id == admin.id: - raise HTTPException(400, "You cannot delete your own account") - - # Prevent deleting the last admin - if target.is_admin and count_admins() <= 1: - raise HTTPException(400, "Cannot delete the last remaining admin") - - delete_user(user_id) - return {"deleted": user_id} - - -@app.put("/api/users/{user_id}/role") -def api_set_role( - user_id: str, - payload: UpdateRolePayload, - admin: User = Depends(require_admin_user), -): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - if target.id == admin.id and payload.role != "admin": - raise HTTPException(400, "You cannot demote your own account") - - if (target.is_admin and payload.role != "admin" - and count_admins() <= 1): - raise HTTPException(400, "Cannot demote the last remaining admin") - - try: - set_user_role(user_id, payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - - return find_user_by_id(user_id).public_dict() - - -@app.put("/api/users/{user_id}/password") -def api_update_password( - user_id: str, - payload: UpdatePasswordPayload, - admin: User = Depends(require_admin_user), -): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - try: - update_password(user_id, payload.password) - except ValueError as e: - raise HTTPException(400, str(e)) - return {"ok": True} - - -# --------------------------------------------------------------------------- -# MFA management -# --------------------------------------------------------------------------- -class ConfirmMfaPayload(BaseModel): - totp_code: str - - -@app.post("/api/users/{user_id}/mfa/begin") -def api_begin_mfa(user_id: str, admin: User = Depends(require_admin_user)): - """Provision a new TOTP secret for the target user (admin action). - Returns the secret + otpauth URI so the admin can show it to the user.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret, uri = begin_mfa_enrollment(user_id) - return { - "secret": secret, - "otpauth_uri": uri, - "username": target.username, - "qr_url": f"/api/users/{user_id}/mfa/qr", - } - - -@app.get("/api/users/{user_id}/mfa/qr") -def api_mfa_qr(user_id: str, admin: User = Depends(require_admin_user)): - """Render a PNG QR code of the target user's current (unconfirmed) TOTP URI.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret = get_totp_secret(user_id) - if not secret: - raise HTTPException(400, "No MFA enrollment in progress") - uri = totp_provisioning_uri(secret, target.username) - - qr = qrcode.QRCode(box_size=6, border=2) - qr.add_data(uri) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - buf = io.BytesIO() - img.save(buf, format="PNG") - buf.seek(0) - return StreamingResponse(buf, media_type="image/png") - - -@app.post("/api/users/{user_id}/mfa/confirm") -def api_confirm_mfa( - user_id: str, - payload: ConfirmMfaPayload, - admin: User = Depends(require_admin_user), -): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - if not confirm_mfa_enrollment(user_id, payload.totp_code): - raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") - return find_user_by_id(user_id).public_dict() - - -@app.post("/api/users/{user_id}/mfa/disable") -def api_disable_mfa(user_id: str, admin: User = Depends(require_admin_user)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - disable_mfa(user_id) - return find_user_by_id(user_id).public_dict() - - -# --------------------------------------------------------------------------- -# Folder management (admin) -# --------------------------------------------------------------------------- -import uuid as _uuid -from datetime import datetime, timezone -from pathlib import Path as _Path -from auth import get_db - - -UPLOAD_DIR = _Path(os.environ.get("DATA_DIR", "/app/data")) / "uploads" - - -class AdminFolderPayload(BaseModel): - name: str - - -def _validate_name(name: str) -> str: - name = (name or "").strip() - if not name: - raise HTTPException(400, "Folder name cannot be empty") - if len(name) > 80: - raise HTTPException(400, "Folder name too long (max 80 characters)") - return name - - -@app.get("/api/folders") -def admin_list_folders(_: User = Depends(require_admin_user)): - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.post("/api/folders") -def admin_create_folder(payload: AdminFolderPayload, - admin: User = Depends(require_admin_user)): - name = _validate_name(payload.name) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) - ).fetchone() - if existing: - raise HTTPException(409, "A folder with that name already exists") - folder_id = _uuid.uuid4().hex - conn.execute( - """INSERT INTO pst_folders (id, name, created_at, created_by) - VALUES (?, ?, ?, ?)""", - (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), - ) - conn.commit() - return {"id": folder_id, "name": name, "file_count": 0} - - -@app.put("/api/folders/{folder_id}") -def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, - admin: User = Depends(require_admin_user)): - name = _validate_name(payload.name) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - clash = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", - (name, folder_id), - ).fetchone() - if clash: - raise HTTPException(409, "A folder with that name already exists") - conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", - (name, folder_id)) - conn.commit() - return {"id": folder_id, "name": name} - - -@app.delete("/api/folders/{folder_id}") -def admin_delete_folder(folder_id: str, - admin: User = Depends(require_admin_user)): - """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) - conn.commit() - return {"deleted": folder_id} - - -# --------------------------------------------------------------------------- -# PST file management (admin) -# --------------------------------------------------------------------------- -@app.get("/api/pst-files") -def admin_list_pst_files(_: User = Depends(require_admin_user)): - with get_db() as conn: - rows = conn.execute( - """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, - p.status, p.message_count, p.error, - p.folder_id, f.name AS folder_name, - p.uploaded_by - FROM pst_files p - LEFT JOIN pst_folders f ON f.id = p.folder_id - ORDER BY p.uploaded_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.delete("/api/pst-files/{pst_id}") -def admin_delete_pst(pst_id: str, admin: User = Depends(require_admin_user)): - with get_db() as conn: - row = conn.execute( - "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - raise HTTPException(404, "PST not found") - conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) - conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) - conn.commit() - try: - (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) - except Exception: - pass - return {"deleted": pst_id} - - -class AdminAssignPayload(BaseModel): - folder_id: Optional[str] = None - - -@app.put("/api/pst-files/{pst_id}/folder") -def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, - admin: User = Depends(require_admin_user)): - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_files WHERE id = ?", - (pst_id,)).fetchone(): - raise HTTPException(404, "PST not found") - if payload.folder_id: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (payload.folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", - (payload.folder_id, pst_id)) - conn.commit() - return {"pst_id": pst_id, "folder_id": payload.folder_id} - - -# --------------------------------------------------------------------------- -# Folder permissions (admin-only) -# Controls which user can access which folder. -# Admins ALWAYS have access to every folder regardless of this table — -# these grants are for non-admin users. -# --------------------------------------------------------------------------- -class PermissionToggle(BaseModel): - granted: bool - - -@app.get("/api/permissions") -def admin_list_permissions(_: User = Depends(require_admin_user)): - """Return the full matrix: users, folders, and all grants.""" - with get_db() as conn: - users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() - folders = conn.execute( - "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" - ).fetchall() - grants = conn.execute( - "SELECT user_id, folder_id, granted_at FROM folder_permissions" - ).fetchall() - - from auth import _row_to_user - user_list = [] - for u in users: - pub = _row_to_user(u).public_dict() - user_list.append(pub) - - return { - "users": user_list, - "folders": [dict(f) for f in folders], - "grants": [dict(g) for g in grants], - } - - -@app.get("/api/users/{user_id}/folders") -def admin_user_folders(user_id: str, _: User = Depends(require_admin_user)): - """Folders a given user has been granted.""" - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, p.granted_at - FROM folder_permissions p - JOIN pst_folders f ON f.id = p.folder_id - WHERE p.user_id = ? - ORDER BY f.name COLLATE NOCASE""", - (user_id,), - ).fetchall() - return { - "user": target.public_dict(), - "folders": [dict(r) for r in rows], - } - - -@app.get("/api/folders/{folder_id}/users") -def admin_folder_users(folder_id: str, _: User = Depends(require_admin_user)): - """Users granted on a given folder.""" - with get_db() as conn: - folder = conn.execute( - "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) - ).fetchone() - if not folder: - raise HTTPException(404, "Folder not found") - - grant_rows = conn.execute( - """SELECT user_id, granted_at - FROM folder_permissions WHERE folder_id = ?""", - (folder_id,), - ).fetchall() - - # Hydrate user details (usernames are encrypted — need decrypt via find_user_by_id) - users_out = [] - for r in grant_rows: - u = find_user_by_id(r["user_id"]) - if u: - users_out.append({ - **u.public_dict(), - "granted_at": r["granted_at"], - }) - users_out.sort(key=lambda x: x["username"].lower()) - return { - "folder": dict(folder), - "users": users_out, - } - - -@app.put("/api/permissions/{user_id}/{folder_id}") -def admin_toggle_permission( - user_id: str, - folder_id: str, - payload: PermissionToggle, - admin: User = Depends(require_admin_user), -): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - - if target.is_admin: - # Admins implicitly have access to everything; refuse explicit grants - # so the UI doesn't confuse admins with checked-but-meaningless rows. - raise HTTPException( - 400, - "Admins have access to all folders automatically; permissions " - "only apply to non-admin users.", - ) - - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - - if payload.granted: - conn.execute( - """INSERT OR IGNORE INTO folder_permissions - (user_id, folder_id, granted_at, granted_by) - VALUES (?, ?, ?, ?)""", - (user_id, folder_id, - datetime.now(timezone.utc).isoformat(), admin.id), - ) - else: - conn.execute( - "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", - (user_id, folder_id), - ) - conn.commit() - - return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} - - -# --------------------------------------------------------------------------- -# Static admin frontend -# --------------------------------------------------------------------------- -if ADMIN_FRONTEND_DIR.exists(): - app.mount("/", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") diff --git a/Forensic-Mail-Indexer/backend/app.py b/Forensic-Mail-Indexer/backend/app.py deleted file mode 100644 index 10cbd89..0000000 --- a/Forensic-Mail-Indexer/backend/app.py +++ /dev/null @@ -1,1234 +0,0 @@ -""" -PST Indexer — Main application (port 8000). - -Serves: -- The web UI (login, setup wizard, archive browser) -- REST API for PST upload, indexing, search -- All data-access endpoints require an authenticated session - -First-run behavior: -- If no users exist, the UI routes users to a setup wizard. -- /api/setup is only callable while the users table is empty; after the first - admin is created, the endpoint returns 409. -""" -import io -import os -import re -import sqlite3 -import uuid -import asyncio -from datetime import datetime, timezone -from pathlib import Path -from typing import Optional - -import aiofiles -import pypff -import qrcode -from fastapi import ( - BackgroundTasks, Depends, FastAPI, File, HTTPException, - Request, Response, UploadFile, status, -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, FileResponse, StreamingResponse -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel - -import auth -from auth import ( - SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, - User, count_users, count_admins, create_user, find_user_by_username, - find_user_by_id, get_totp_secret, verify_password, verify_totp, - record_login, create_session_token, login_limiter, require_app_user, - require_app_admin, get_db, - # Administration (merged in from the former standalone admin panel) - begin_mfa_enrollment, confirm_mfa_enrollment, delete_user, disable_mfa, - list_users, set_user_role, totp_provisioning_uri, update_password, -) - -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- -DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) -UPLOAD_DIR = DATA_DIR / "uploads" -FRONTEND_DIR = Path(os.environ.get("FRONTEND_DIR", "/app/frontend")) -ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) - -UPLOAD_DIR.mkdir(parents=True, exist_ok=True) -DATA_DIR.mkdir(parents=True, exist_ok=True) - -# Cookie security. Flip to True when serving over HTTPS. -COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" - - -# --------------------------------------------------------------------------- -# App DB schema (messages + pst_files) -# --------------------------------------------------------------------------- -def init_app_schema(): - with get_db() as conn: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS pst_folders ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - created_at TEXT NOT NULL, - created_by TEXT - ); - - CREATE TABLE IF NOT EXISTS folder_permissions ( - user_id TEXT NOT NULL, - folder_id TEXT NOT NULL, - granted_at TEXT NOT NULL, - granted_by TEXT, - PRIMARY KEY (user_id, folder_id) - ); - - CREATE INDEX IF NOT EXISTS idx_folder_perms_user ON folder_permissions(user_id); - CREATE INDEX IF NOT EXISTS idx_folder_perms_folder ON folder_permissions(folder_id); - - CREATE TABLE IF NOT EXISTS pst_files ( - id TEXT PRIMARY KEY, - filename TEXT NOT NULL, - original_name TEXT NOT NULL, - uploaded_at TEXT NOT NULL, - size_bytes INTEGER NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - message_count INTEGER DEFAULT 0, - error TEXT, - uploaded_by TEXT, - folder_id TEXT - ); - - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pst_id TEXT NOT NULL, - folder TEXT, - subject TEXT, - sender TEXT, - recipients TEXT, - sent_date TEXT, - body TEXT, - attachments TEXT, - FOREIGN KEY (pst_id) REFERENCES pst_files(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_messages_pst ON messages(pst_id); - CREATE INDEX IF NOT EXISTS idx_messages_sent_date ON messages(sent_date); - CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id); - - CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( - subject, sender, recipients, body, folder, - content='messages', - content_rowid='id', - tokenize='porter unicode61' - ); - - CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN - INSERT INTO messages_fts(rowid, subject, sender, recipients, body, folder) - VALUES (new.id, new.subject, new.sender, new.recipients, new.body, new.folder); - END; - - CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN - INSERT INTO messages_fts(messages_fts, rowid, subject, sender, recipients, body, folder) - VALUES ('delete', old.id, old.subject, old.sender, old.recipients, old.body, old.folder); - END; - """) - # Backfill: add folder_id column to DBs created before this feature existed. - cols = [r["name"] for r in conn.execute("PRAGMA table_info(pst_files)").fetchall()] - if "folder_id" not in cols: - conn.execute("ALTER TABLE pst_files ADD COLUMN folder_id TEXT") - conn.execute("CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id)") - conn.commit() - - -# --------------------------------------------------------------------------- -# Access control helpers — which folders / PSTs is a given user allowed to see? -# -# Rules: -# - Admins see everything. -# - Regular users see only folders they have been explicitly granted, -# plus the implicit "Uncategorized" bucket (folder_id = NULL) — this is -# deliberately shared because otherwise newly-uploaded PSTs would be -# invisible until someone moved them into a permitted folder. -# - A PST is visible iff its folder is visible (or the PST is uncategorized). -# --------------------------------------------------------------------------- -def allowed_folder_ids_for(user) -> set[str]: - """Return the set of folder_ids a user is explicitly permitted on. - For admins this is not used — they see everything unconditionally.""" - with get_db() as conn: - rows = conn.execute( - "SELECT folder_id FROM folder_permissions WHERE user_id = ?", - (user.id,), - ).fetchall() - return {r["folder_id"] for r in rows} - - -def user_can_see_folder(user, folder_id: Optional[str]) -> bool: - """Can this user see content inside this folder? folder_id may be None - (Uncategorized), which is visible to everyone.""" - if user.is_admin: - return True - if folder_id is None: - return True # Uncategorized is shared - return folder_id in allowed_folder_ids_for(user) - - -def user_can_see_pst(user, pst_id: str) -> bool: - if user.is_admin: - return True - with get_db() as conn: - row = conn.execute( - "SELECT folder_id FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - return False - return user_can_see_folder(user, row["folder_id"]) - - -# --------------------------------------------------------------------------- -# PST indexing helpers (unchanged from pre-auth version) -# --------------------------------------------------------------------------- -def _safe_str(value) -> str: - if value is None: - return "" - if isinstance(value, bytes): - try: - return value.decode("utf-8", errors="replace") - except Exception: - return value.decode("latin-1", errors="replace") - return str(value) - - -def _extract_recipients(message) -> str: - parts = [] - try: - headers = _safe_str(getattr(message, "transport_headers", "")) - if headers: - for line in headers.split("\n"): - low = line.lower() - if low.startswith("to:") or low.startswith("cc:") or low.startswith("bcc:"): - parts.append(line.strip()) - except Exception: - pass - return " | ".join(parts) if parts else "" - - -def _extract_body(message) -> str: - for attr in ("plain_text_body", "html_body", "rtf_body"): - try: - body = getattr(message, attr, None) - if body: - text = _safe_str(body) - if attr == "html_body": - text = re.sub(r"<[^>]+>", " ", text) - text = re.sub(r"\s+", " ", text).strip() - if text: - return text - except Exception: - continue - return "" - - -def _extract_sender(message) -> str: - for attr in ("sender_name", "sender_email_address"): - try: - v = _safe_str(getattr(message, attr, "")) - if v: - return v - except Exception: - continue - try: - headers = _safe_str(getattr(message, "transport_headers", "")) - for line in headers.split("\n"): - if line.lower().startswith("from:"): - return line.split(":", 1)[1].strip() - except Exception: - pass - return "" - - -def _extract_date(message) -> Optional[str]: - try: - d = getattr(message, "delivery_time", None) or getattr(message, "client_submit_time", None) - if d: - if isinstance(d, datetime): - return d.isoformat() - return _safe_str(d) - except Exception: - pass - return None - - -def _extract_attachments(message) -> str: - names = [] - try: - count = getattr(message, "number_of_attachments", 0) - for i in range(count): - try: - att = message.get_attachment(i) - name = _safe_str(getattr(att, "name", "") or getattr(att, "long_filename", "")) - if name: - names.append(name) - except Exception: - continue - except Exception: - pass - return " | ".join(names) - - -def _walk_folders(folder, path=""): - try: - name = _safe_str(getattr(folder, "name", "") or "") - except Exception: - name = "" - current_path = f"{path}/{name}" if name else (path or "/") - try: - for i in range(folder.number_of_sub_messages): - try: - msg = folder.get_sub_message(i) - yield current_path, msg - except Exception: - continue - except Exception: - pass - try: - for i in range(folder.number_of_sub_folders): - try: - sub = folder.get_sub_folder(i) - yield from _walk_folders(sub, current_path) - except Exception: - continue - except Exception: - pass - - -def index_pst_file(pst_id: str, file_path: Path): - pff = None - try: - pff = pypff.file() - pff.open(str(file_path)) - root = pff.get_root_folder() - count = 0 - batch = [] - BATCH_SIZE = 500 - - with get_db() as conn: - for folder_path, msg in _walk_folders(root): - try: - batch.append(( - pst_id, folder_path, - _safe_str(getattr(msg, "subject", "")), - _extract_sender(msg), - _extract_recipients(msg), - _extract_date(msg), - _extract_body(msg), - _extract_attachments(msg), - )) - count += 1 - if len(batch) >= BATCH_SIZE: - conn.executemany( - """INSERT INTO messages - (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - batch, - ) - conn.commit() - batch.clear() - except Exception as e: - print(f"[index] skip: {e}") - continue - - if batch: - conn.executemany( - """INSERT INTO messages - (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - batch, - ) - conn.commit() - - conn.execute( - "UPDATE pst_files SET status = ?, message_count = ? WHERE id = ?", - ("ready", count, pst_id), - ) - conn.commit() - - print(f"[index] {pst_id}: {count} messages") - except Exception as e: - print(f"[index] failed: {e}") - with get_db() as conn: - conn.execute( - "UPDATE pst_files SET status = ?, error = ? WHERE id = ?", - ("failed", str(e), pst_id), - ) - conn.commit() - finally: - if pff is not None: - try: - pff.close() - except Exception: - pass - - -# --------------------------------------------------------------------------- -# FastAPI -# --------------------------------------------------------------------------- -app = FastAPI(title="PST Indexer") - -app.add_middleware( - CORSMiddleware, - allow_origins=[], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -def _startup(): - auth.init_auth_schema() - init_app_schema() - - -def _set_session_cookie(response: Response, token: str): - response.set_cookie( - key=SESSION_COOKIE_NAME, - value=token, - max_age=SESSION_TTL_SECONDS, - httponly=True, - secure=COOKIE_SECURE, - samesite="lax", - path="/", - ) - - -def _clear_session_cookie(response: Response): - response.delete_cookie(key=SESSION_COOKIE_NAME, path="/") - - -# ----- Setup / auth endpoints ----- - -class SetupRequest(BaseModel): - username: str - password: str - - -class LoginRequest(BaseModel): - username: str - password: str - totp_code: Optional[str] = None - - -@app.get("/api/setup/status") -def setup_status(): - """Lets the frontend decide whether to show the setup wizard.""" - return {"needs_setup": count_users() == 0} - - -@app.post("/api/setup") -def setup_admin(payload: SetupRequest, response: Response): - if count_users() > 0: - raise HTTPException(status.HTTP_409_CONFLICT, "Setup has already been completed") - - try: - user = create_user(payload.username, payload.password, role="admin") - except ValueError as e: - raise HTTPException(400, str(e)) - - record_login(user.id) - token = create_session_token(user, scope="app") - _set_session_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/login") -def login(payload: LoginRequest, request: Request, response: Response): - ip = request.client.host if request.client else "unknown" - rate_key = f"{ip}:{payload.username.lower()}" - if not login_limiter.check(rate_key): - raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") - - found = find_user_by_username(payload.username) - if not found: - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - user, stored_pw_hash = found - if not verify_password(stored_pw_hash, payload.password): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid credentials") - - if user.mfa_enabled: - if not payload.totp_code: - # Tell the client MFA is required - return JSONResponse( - status_code=401, - content={"detail": "MFA required", "mfa_required": True}, - ) - secret = get_totp_secret(user.id) - if not secret or not verify_totp(secret, payload.totp_code): - login_limiter.record(rate_key) - raise HTTPException(401, "Invalid MFA code") - - login_limiter.reset(rate_key) - record_login(user.id) - token = create_session_token(user, scope="app") - _set_session_cookie(response, token) - return {"user": user.public_dict()} - - -@app.post("/api/logout") -def logout(response: Response): - _clear_session_cookie(response) - return {"ok": True} - - -@app.get("/api/me") -def me(user: User = Depends(require_app_user)): - return {"user": user.public_dict()} - - -# ----- PST endpoints (all require auth) ----- - -@app.get("/api/health") -def health(): - return {"status": "ok"} - - -@app.get("/api/pst-files") -def list_pst_files(user: User = Depends(require_app_user)): - with get_db() as conn: - if user.is_admin: - rows = conn.execute( - """SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files ORDER BY uploaded_at DESC""" - ).fetchall() - else: - allowed = allowed_folder_ids_for(user) - if allowed: - placeholders = ",".join("?" for _ in allowed) - rows = conn.execute( - f"""SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files - WHERE folder_id IS NULL OR folder_id IN ({placeholders}) - ORDER BY uploaded_at DESC""", - list(allowed), - ).fetchall() - else: - rows = conn.execute( - """SELECT id, original_name, uploaded_at, size_bytes, status, - message_count, error, folder_id - FROM pst_files WHERE folder_id IS NULL - ORDER BY uploaded_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -# ----- Folder endpoints (shared with admin panel) ----- - -class FolderCreate(BaseModel): - name: str - - -class FolderRename(BaseModel): - name: str - - -class FolderAssign(BaseModel): - folder_id: Optional[str] = None # null = move to uncategorized - - -def _validate_folder_name(name: str) -> str: - name = (name or "").strip() - if not name: - raise HTTPException(400, "Folder name cannot be empty") - if len(name) > 80: - raise HTTPException(400, "Folder name too long (max 80 characters)") - return name - - -@app.get("/api/folders") -def list_folders(user: User = Depends(require_app_user)): - with get_db() as conn: - if user.is_admin: - rows = conn.execute( - """SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""" - ).fetchall() - else: - allowed = allowed_folder_ids_for(user) - if not allowed: - return [] - placeholders = ",".join("?" for _ in allowed) - rows = conn.execute( - f"""SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - WHERE f.id IN ({placeholders}) - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""", - list(allowed), - ).fetchall() - return [dict(r) for r in rows] - - -@app.post("/api/folders") -def create_folder(payload: FolderCreate, user: User = Depends(require_app_user)): - # Only admins can create folders — the admin panel is the authoritative - # management surface. (We still allow creation from the main app sidebar, - # but only for admin users.) - if not user.is_admin: - raise HTTPException(403, "Only administrators can create folders") - name = _validate_folder_name(payload.name) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) - ).fetchone() - if existing: - raise HTTPException(409, "A folder with that name already exists") - folder_id = uuid.uuid4().hex - conn.execute( - """INSERT INTO pst_folders (id, name, created_at, created_by) - VALUES (?, ?, ?, ?)""", - (folder_id, name, datetime.now(timezone.utc).isoformat(), user.id), - ) - conn.commit() - return {"id": folder_id, "name": name, "file_count": 0} - - -@app.put("/api/folders/{folder_id}") -def rename_folder(folder_id: str, payload: FolderRename, - user: User = Depends(require_app_user)): - if not user.is_admin: - raise HTTPException(403, "Only administrators can rename folders") - name = _validate_folder_name(payload.name) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - clash = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", - (name, folder_id), - ).fetchone() - if clash: - raise HTTPException(409, "A folder with that name already exists") - conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", - (name, folder_id)) - conn.commit() - return {"id": folder_id, "name": name} - - -@app.delete("/api/folders/{folder_id}") -def delete_folder(folder_id: str, user: User = Depends(require_app_user)): - """Delete a folder. Any PSTs inside it become uncategorized (folder_id = NULL).""" - if not user.is_admin: - raise HTTPException(403, "Only administrators can delete folders") - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) - conn.commit() - return {"deleted": folder_id} - - -@app.put("/api/pst-files/{pst_id}/folder") -def assign_pst_to_folder(pst_id: str, payload: FolderAssign, - user: User = Depends(require_app_user)): - # Check the user can see the PST's current location AND the destination - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - if payload.folder_id and not user_can_see_folder(user, payload.folder_id): - raise HTTPException(403, "You do not have access to the target folder") - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_files WHERE id = ?", - (pst_id,)).fetchone(): - raise HTTPException(404, "PST not found") - if payload.folder_id: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (payload.folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", - (payload.folder_id, pst_id)) - conn.commit() - return {"pst_id": pst_id, "folder_id": payload.folder_id} - - -@app.post("/api/pst-files") -async def upload_pst( - background_tasks: BackgroundTasks, - file: UploadFile = File(...), - folder_id: Optional[str] = None, - user: User = Depends(require_app_user), -): - if not file.filename: - raise HTTPException(400, "No filename provided") - if not file.filename.lower().endswith((".pst", ".ost")): - raise HTTPException(400, "Only .pst and .ost files are supported") - - # Validate folder if provided (and verify user has access to it) - if folder_id: - with get_db() as conn: - exists = conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone() - if not exists: - raise HTTPException(400, "Target folder does not exist") - if not user_can_see_folder(user, folder_id): - raise HTTPException(403, "You do not have access to the target folder") - - pst_id = uuid.uuid4().hex - stored_name = f"{pst_id}_{file.filename}" - stored_path = UPLOAD_DIR / stored_name - - size = 0 - async with aiofiles.open(stored_path, "wb") as out: - while chunk := await file.read(1024 * 1024): - await out.write(chunk) - size += len(chunk) - - with get_db() as conn: - conn.execute( - """INSERT INTO pst_files - (id, filename, original_name, uploaded_at, size_bytes, status, - uploaded_by, folder_id) - VALUES (?, ?, ?, ?, ?, 'indexing', ?, ?)""", - (pst_id, stored_name, file.filename, - datetime.now(timezone.utc).isoformat(), size, user.id, folder_id), - ) - conn.commit() - - background_tasks.add_task(asyncio.to_thread, index_pst_file, pst_id, stored_path) - return {"id": pst_id, "original_name": file.filename, - "status": "indexing", "size_bytes": size, "folder_id": folder_id} - - -@app.delete("/api/pst-files/{pst_id}") -def delete_pst(pst_id: str, user: User = Depends(require_app_user)): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - with get_db() as conn: - row = conn.execute( - "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - raise HTTPException(404, "PST not found") - conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) - conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) - conn.commit() - try: - (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) - except Exception: - pass - return {"deleted": pst_id} - - -@app.get("/api/pst-files/{pst_id}/folders") -def get_folders(pst_id: str, user: User = Depends(require_app_user)): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - with get_db() as conn: - rows = conn.execute( - """SELECT folder, COUNT(*) as count FROM messages - WHERE pst_id = ? GROUP BY folder ORDER BY folder""", - (pst_id,), - ).fetchall() - return [dict(r) for r in rows] - - -def _escape_fts(query: str) -> str: - tokens = [] - for raw in query.split(): - if re.fullmatch(r"[A-Za-z0-9]+", raw): - tokens.append(f'"{raw}"*') - else: - sanitized = raw.replace('"', '""') - tokens.append(f'"{sanitized}"') - return " ".join(tokens) - - -@app.get("/api/pst-files/{pst_id}/messages") -def search_messages( - pst_id: str, - q: Optional[str] = None, - folder: Optional[str] = None, - limit: int = 50, - offset: int = 0, - user: User = Depends(require_app_user), -): - if not user_can_see_pst(user, pst_id): - raise HTTPException(404, "PST not found") - limit = max(1, min(limit, 200)) - offset = max(0, offset) - - with get_db() as conn: - p = conn.execute("SELECT id FROM pst_files WHERE id = ?", (pst_id,)).fetchone() - if not p: - raise HTTPException(404, "PST not found") - - params = [pst_id] - - if q and q.strip(): - fts_q = _escape_fts(q.strip()) - base = """ - FROM messages m - JOIN messages_fts f ON f.rowid = m.id - WHERE m.pst_id = ? AND messages_fts MATCH ? - """ - params.append(fts_q) - if folder: - base += " AND m.folder = ?" - params.append(folder) - - total = conn.execute("SELECT COUNT(*) as c " + base, params).fetchone()["c"] - - data_sql = ( - "SELECT m.id, m.folder, m.subject, m.sender, m.recipients, m.sent_date, " - " m.attachments, snippet(messages_fts, 3, '', '', '…', 24) AS snippet " - + base + " ORDER BY rank LIMIT ? OFFSET ?" - ) - rows = conn.execute(data_sql, params + [limit, offset]).fetchall() - else: - where = ["m.pst_id = ?"] - if folder: - where.append("m.folder = ?") - params.append(folder) - where_sql = " AND ".join(where) - total = conn.execute( - f"SELECT COUNT(*) as c FROM messages m WHERE {where_sql}", params - ).fetchone()["c"] - rows = conn.execute( - f"""SELECT m.id, m.folder, m.subject, m.sender, m.recipients, - m.sent_date, m.attachments, substr(m.body, 1, 200) AS snippet - FROM messages m WHERE {where_sql} - ORDER BY m.sent_date DESC LIMIT ? OFFSET ?""", - params + [limit, offset], - ).fetchall() - - return {"total": total, "limit": limit, "offset": offset, - "items": [dict(r) for r in rows]} - - -@app.get("/api/messages/{message_id}") -def get_message(message_id: int, user: User = Depends(require_app_user)): - with get_db() as conn: - row = conn.execute( - """SELECT id, pst_id, folder, subject, sender, recipients, - sent_date, body, attachments - FROM messages WHERE id = ?""", - (message_id,), - ).fetchone() - if not row: - raise HTTPException(404, "Message not found") - if not user_can_see_pst(user, row["pst_id"]): - raise HTTPException(404, "Message not found") - return dict(row) - - -# =========================================================================== -# ADMINISTRATION SECTION -# -# Previously a separate FastAPI process (admin_app.py) on its own port. It now -# runs inside the main application. Every endpoint below is namespaced under -# /api/admin and protected by `require_app_admin`, which authenticates against -# the main app session cookie AND requires the admin role. Non-admin users -# receive 403; unauthenticated requests receive 401. -# -# The administration UI is served at /admin (static files), and the main app -# shows an "ADMINISTRATION" button (admins only) that links to it. -# =========================================================================== - -# ----- Admin: user management ----- - -class CreateUserPayload(BaseModel): - username: str - password: str - role: str = "user" - - -class UpdatePasswordPayload(BaseModel): - password: str - - -class UpdateRolePayload(BaseModel): - role: str - - -@app.get("/api/admin/me") -def admin_me(user: User = Depends(require_app_admin)): - return {"user": user.public_dict()} - - -@app.get("/api/admin/users") -def admin_list_users(_: User = Depends(require_app_admin)): - return [u.public_dict() for u in list_users()] - - -@app.post("/api/admin/users") -def admin_create_user(payload: CreateUserPayload, - admin: User = Depends(require_app_admin)): - try: - user = create_user(payload.username, payload.password, role=payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - return user.public_dict() - - -@app.delete("/api/admin/users/{user_id}") -def admin_delete_user(user_id: str, admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - if target.id == admin.id: - raise HTTPException(400, "You cannot delete your own account") - if target.is_admin and count_admins() <= 1: - raise HTTPException(400, "Cannot delete the last remaining admin") - delete_user(user_id) - return {"deleted": user_id} - - -@app.put("/api/admin/users/{user_id}/role") -def admin_set_role(user_id: str, payload: UpdateRolePayload, - admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - if target.id == admin.id and payload.role != "admin": - raise HTTPException(400, "You cannot demote your own account") - if (target.is_admin and payload.role != "admin" and count_admins() <= 1): - raise HTTPException(400, "Cannot demote the last remaining admin") - try: - set_user_role(user_id, payload.role) - except ValueError as e: - raise HTTPException(400, str(e)) - return find_user_by_id(user_id).public_dict() - - -@app.put("/api/admin/users/{user_id}/password") -def admin_update_password(user_id: str, payload: UpdatePasswordPayload, - admin: User = Depends(require_app_admin)): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - try: - update_password(user_id, payload.password) - except ValueError as e: - raise HTTPException(400, str(e)) - return {"ok": True} - - -# ----- Admin: MFA management ----- - -class ConfirmMfaPayload(BaseModel): - totp_code: str - - -@app.post("/api/admin/users/{user_id}/mfa/begin") -def admin_begin_mfa(user_id: str, admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret, uri = begin_mfa_enrollment(user_id) - return { - "secret": secret, - "otpauth_uri": uri, - "username": target.username, - "qr_url": f"/api/admin/users/{user_id}/mfa/qr", - } - - -@app.get("/api/admin/users/{user_id}/mfa/qr") -def admin_mfa_qr(user_id: str, admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - secret = get_totp_secret(user_id) - if not secret: - raise HTTPException(400, "No MFA enrollment in progress") - uri = totp_provisioning_uri(secret, target.username) - - qr = qrcode.QRCode(box_size=6, border=2) - qr.add_data(uri) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - buf = io.BytesIO() - img.save(buf, format="PNG") - buf.seek(0) - return StreamingResponse(buf, media_type="image/png") - - -@app.post("/api/admin/users/{user_id}/mfa/confirm") -def admin_confirm_mfa(user_id: str, payload: ConfirmMfaPayload, - admin: User = Depends(require_app_admin)): - if not find_user_by_id(user_id): - raise HTTPException(404, "User not found") - if not confirm_mfa_enrollment(user_id, payload.totp_code): - raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") - return find_user_by_id(user_id).public_dict() - - -@app.post("/api/admin/users/{user_id}/mfa/disable") -def admin_disable_mfa(user_id: str, admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - disable_mfa(user_id) - return find_user_by_id(user_id).public_dict() - - -# ----- Admin: folder management (sees ALL folders unconditionally) ----- - -class AdminFolderPayload(BaseModel): - name: str - - -@app.get("/api/admin/folders") -def admin_list_all_folders(_: User = Depends(require_app_admin)): - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, f.created_at, f.created_by, - COUNT(p.id) AS file_count - FROM pst_folders f - LEFT JOIN pst_files p ON p.folder_id = f.id - GROUP BY f.id - ORDER BY f.name COLLATE NOCASE""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.post("/api/admin/folders") -def admin_create_folder(payload: AdminFolderPayload, - admin: User = Depends(require_app_admin)): - name = _validate_folder_name(payload.name) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) - ).fetchone() - if existing: - raise HTTPException(409, "A folder with that name already exists") - folder_id = uuid.uuid4().hex - conn.execute( - """INSERT INTO pst_folders (id, name, created_at, created_by) - VALUES (?, ?, ?, ?)""", - (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), - ) - conn.commit() - return {"id": folder_id, "name": name, "file_count": 0} - - -@app.put("/api/admin/folders/{folder_id}") -def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, - admin: User = Depends(require_app_admin)): - name = _validate_folder_name(payload.name) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - clash = conn.execute( - "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", - (name, folder_id), - ).fetchone() - if clash: - raise HTTPException(409, "A folder with that name already exists") - conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", - (name, folder_id)) - conn.commit() - return {"id": folder_id, "name": name} - - -@app.delete("/api/admin/folders/{folder_id}") -def admin_delete_folder(folder_id: str, admin: User = Depends(require_app_admin)): - """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", - (folder_id,)) - conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) - conn.commit() - return {"deleted": folder_id} - - -# ----- Admin: PST file management (sees ALL files unconditionally) ----- - -@app.get("/api/admin/pst-files") -def admin_list_pst_files(_: User = Depends(require_app_admin)): - with get_db() as conn: - rows = conn.execute( - """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, - p.status, p.message_count, p.error, - p.folder_id, f.name AS folder_name, - p.uploaded_by - FROM pst_files p - LEFT JOIN pst_folders f ON f.id = p.folder_id - ORDER BY p.uploaded_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -@app.delete("/api/admin/pst-files/{pst_id}") -def admin_delete_pst(pst_id: str, admin: User = Depends(require_app_admin)): - with get_db() as conn: - row = conn.execute( - "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) - ).fetchone() - if not row: - raise HTTPException(404, "PST not found") - conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) - conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) - conn.commit() - try: - (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) - except Exception: - pass - return {"deleted": pst_id} - - -class AdminAssignPayload(BaseModel): - folder_id: Optional[str] = None - - -@app.put("/api/admin/pst-files/{pst_id}/folder") -def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, - admin: User = Depends(require_app_admin)): - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_files WHERE id = ?", - (pst_id,)).fetchone(): - raise HTTPException(404, "PST not found") - if payload.folder_id: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (payload.folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", - (payload.folder_id, pst_id)) - conn.commit() - return {"pst_id": pst_id, "folder_id": payload.folder_id} - - -# ----- Admin: folder access permissions ----- - -class PermissionToggle(BaseModel): - granted: bool - - -@app.get("/api/admin/permissions") -def admin_list_permissions(_: User = Depends(require_app_admin)): - """Return the full matrix: users, folders, and all grants.""" - with get_db() as conn: - users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() - folders = conn.execute( - "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" - ).fetchall() - grants = conn.execute( - "SELECT user_id, folder_id, granted_at FROM folder_permissions" - ).fetchall() - - from auth import _row_to_user - user_list = [_row_to_user(u).public_dict() for u in users] - return { - "users": user_list, - "folders": [dict(f) for f in folders], - "grants": [dict(g) for g in grants], - } - - -@app.get("/api/admin/users/{user_id}/folders") -def admin_user_folders(user_id: str, _: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - with get_db() as conn: - rows = conn.execute( - """SELECT f.id, f.name, p.granted_at - FROM folder_permissions p - JOIN pst_folders f ON f.id = p.folder_id - WHERE p.user_id = ? - ORDER BY f.name COLLATE NOCASE""", - (user_id,), - ).fetchall() - return {"user": target.public_dict(), "folders": [dict(r) for r in rows]} - - -@app.get("/api/admin/folders/{folder_id}/users") -def admin_folder_users(folder_id: str, _: User = Depends(require_app_admin)): - with get_db() as conn: - folder = conn.execute( - "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) - ).fetchone() - if not folder: - raise HTTPException(404, "Folder not found") - grant_rows = conn.execute( - """SELECT user_id, granted_at - FROM folder_permissions WHERE folder_id = ?""", - (folder_id,), - ).fetchall() - - users_out = [] - for r in grant_rows: - u = find_user_by_id(r["user_id"]) - if u: - users_out.append({**u.public_dict(), "granted_at": r["granted_at"]}) - users_out.sort(key=lambda x: x["username"].lower()) - return {"folder": dict(folder), "users": users_out} - - -@app.put("/api/admin/permissions/{user_id}/{folder_id}") -def admin_toggle_permission(user_id: str, folder_id: str, - payload: PermissionToggle, - admin: User = Depends(require_app_admin)): - target = find_user_by_id(user_id) - if not target: - raise HTTPException(404, "User not found") - if target.is_admin: - raise HTTPException( - 400, - "Admins have access to all folders automatically; permissions " - "only apply to non-admin users.", - ) - with get_db() as conn: - if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", - (folder_id,)).fetchone(): - raise HTTPException(404, "Folder not found") - if payload.granted: - conn.execute( - """INSERT OR IGNORE INTO folder_permissions - (user_id, folder_id, granted_at, granted_by) - VALUES (?, ?, ?, ?)""", - (user_id, folder_id, - datetime.now(timezone.utc).isoformat(), admin.id), - ) - else: - conn.execute( - "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", - (user_id, folder_id), - ) - conn.commit() - return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} - - -# --------------------------------------------------------------------------- -# Static frontends -# -# The administration UI is mounted at /admin and MUST be registered before the -# catch-all "/" mount so that /admin/* resolves to the admin assets. Both are -# served from the same origin/port, so they share the main session cookie. -# --------------------------------------------------------------------------- -if ADMIN_FRONTEND_DIR.exists(): - app.mount("/admin", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") - -# --------------------------------------------------------------------------- -# Static frontend (anonymous access — but index.html enforces auth client-side -# AND the APIs enforce it server-side, which is what actually matters) -# --------------------------------------------------------------------------- -if FRONTEND_DIR.exists(): - app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend") diff --git a/Forensic-Mail-Indexer/backend/auth.py b/Forensic-Mail-Indexer/backend/auth.py deleted file mode 100644 index 9937c82..0000000 --- a/Forensic-Mail-Indexer/backend/auth.py +++ /dev/null @@ -1,520 +0,0 @@ -""" -auth.py — Shared authentication & encryption module. - -Responsibilities: -- Derive or generate a master secret key (persisted to disk with chmod 600) -- Provide Fernet-based encryption for user PII at rest -- Argon2id password hashing (constant-time verification) -- TOTP secret generation, QR provisioning URI, 6-digit code verification -- JWT session tokens (signed with master key) -- User CRUD on the shared SQLite database -- FastAPI dependencies to enforce authentication and admin role -""" -from __future__ import annotations - -import base64 -import hashlib -import hmac -import os -import secrets -import sqlite3 -import time -from contextlib import contextmanager -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Optional - -import jwt -import pyotp -from argon2 import PasswordHasher -from argon2.exceptions import VerifyMismatchError, InvalidHashError -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from fastapi import Depends, HTTPException, Request, status - -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- -DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) -DATA_DIR.mkdir(parents=True, exist_ok=True) - -SECRET_KEY_PATH = DATA_DIR / "secret.key" -DB_PATH = DATA_DIR / "pst_index.db" - -SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", 60 * 60 * 8)) # 8 hours -SESSION_COOKIE_NAME = "pst_session" -ADMIN_SESSION_COOKIE_NAME = "pst_admin_session" - -JWT_ALGORITHM = "HS256" - -# --------------------------------------------------------------------------- -# Master secret -# --------------------------------------------------------------------------- -def _load_or_create_master_secret() -> bytes: - """Return a 32-byte master secret. Created on first run, persisted to disk.""" - # Allow overriding via env var (useful for orchestration); still persist so - # restarts without the env var keep working. - env_secret = os.environ.get("MASTER_SECRET") - if env_secret: - return hashlib.sha256(env_secret.encode("utf-8")).digest() - - if SECRET_KEY_PATH.exists(): - data = SECRET_KEY_PATH.read_bytes() - if len(data) == 32: - return data - # Legacy or truncated: rewrap - return hashlib.sha256(data).digest() - - secret = secrets.token_bytes(32) - SECRET_KEY_PATH.write_bytes(secret) - try: - os.chmod(SECRET_KEY_PATH, 0o600) - except Exception: - pass - return secret - - -MASTER_SECRET: bytes = _load_or_create_master_secret() - - -def _derive_subkey(info: bytes, length: int = 32) -> bytes: - """Derive a sub-key from the master secret using HKDF-SHA256.""" - return HKDF( - algorithm=hashes.SHA256(), - length=length, - salt=b"pst-indexer-salt-v1", - info=info, - ).derive(MASTER_SECRET) - - -# Key used for encrypting user PII at rest -_FERNET_KEY = base64.urlsafe_b64encode(_derive_subkey(b"fernet-user-pii")) -_FERNET = Fernet(_FERNET_KEY) - -# Key used for JWT signatures -_JWT_KEY = _derive_subkey(b"jwt-sessions") - -# Key used for deterministic username lookup hashes (so we can index without -# exposing plaintext usernames in the DB). -_USERNAME_HMAC_KEY = _derive_subkey(b"username-lookup") - - -# --------------------------------------------------------------------------- -# Encryption helpers -# --------------------------------------------------------------------------- -def encrypt(plaintext: str) -> str: - """Fernet-encrypt a string; returns base64 ciphertext.""" - if plaintext is None: - return "" - return _FERNET.encrypt(plaintext.encode("utf-8")).decode("utf-8") - - -def decrypt(ciphertext: str) -> str: - """Fernet-decrypt. Returns empty string for empty input.""" - if not ciphertext: - return "" - try: - return _FERNET.decrypt(ciphertext.encode("utf-8")).decode("utf-8") - except InvalidToken: - raise ValueError("Failed to decrypt — master key may have changed") - - -def username_lookup_hash(username: str) -> str: - """Deterministic HMAC-SHA256 over the normalized username, hex-encoded.""" - normalized = username.strip().lower().encode("utf-8") - return hmac.new(_USERNAME_HMAC_KEY, normalized, hashlib.sha256).hexdigest() - - -# --------------------------------------------------------------------------- -# Password hashing -# --------------------------------------------------------------------------- -_hasher = PasswordHasher( - time_cost=3, - memory_cost=64 * 1024, # 64 MiB - parallelism=2, -) - - -def hash_password(password: str) -> str: - return _hasher.hash(password) - - -def verify_password(stored_hash: str, password: str) -> bool: - try: - _hasher.verify(stored_hash, password) - return True - except (VerifyMismatchError, InvalidHashError): - return False - except Exception: - return False - - -# --------------------------------------------------------------------------- -# TOTP (MFA) -# --------------------------------------------------------------------------- -def generate_totp_secret() -> str: - return pyotp.random_base32() - - -def totp_provisioning_uri(secret: str, username: str, issuer: str = "PST Archive") -> str: - return pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer) - - -def verify_totp(secret: str, code: str) -> bool: - if not secret or not code: - return False - try: - return pyotp.TOTP(secret).verify(code.strip(), valid_window=1) - except Exception: - return False - - -# --------------------------------------------------------------------------- -# Database -# --------------------------------------------------------------------------- -@contextmanager -def get_db(): - conn = sqlite3.connect(str(DB_PATH)) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA foreign_keys = ON") - try: - yield conn - finally: - conn.close() - - -def init_auth_schema(): - with get_db() as conn: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username_hash TEXT NOT NULL UNIQUE, - username_enc TEXT NOT NULL, - password_hash_enc TEXT NOT NULL, - totp_secret_enc TEXT, - mfa_enabled INTEGER NOT NULL DEFAULT 0, - role TEXT NOT NULL DEFAULT 'user', - created_at TEXT NOT NULL, - last_login TEXT - ); - - CREATE INDEX IF NOT EXISTS idx_users_username_hash ON users(username_hash); - CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); - """) - conn.commit() - - -# --------------------------------------------------------------------------- -# User model -# --------------------------------------------------------------------------- -@dataclass -class User: - id: str - username: str - role: str - mfa_enabled: bool - created_at: str - last_login: Optional[str] - - @property - def is_admin(self) -> bool: - return self.role == "admin" - - def public_dict(self) -> dict: - return { - "id": self.id, - "username": self.username, - "role": self.role, - "mfa_enabled": self.mfa_enabled, - "created_at": self.created_at, - "last_login": self.last_login, - } - - -def _row_to_user(row: sqlite3.Row) -> User: - return User( - id=row["id"], - username=decrypt(row["username_enc"]), - role=row["role"], - mfa_enabled=bool(row["mfa_enabled"]), - created_at=row["created_at"], - last_login=row["last_login"], - ) - - -def count_users() -> int: - with get_db() as conn: - return conn.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] - - -def count_admins() -> int: - with get_db() as conn: - return conn.execute("SELECT COUNT(*) AS c FROM users WHERE role='admin'").fetchone()["c"] - - -def find_user_by_username(username: str) -> Optional[tuple[User, str]]: - """Return (User, decrypted_password_hash) if found, else None.""" - uh = username_lookup_hash(username) - with get_db() as conn: - row = conn.execute( - "SELECT * FROM users WHERE username_hash = ?", (uh,) - ).fetchone() - if not row: - return None - user = _row_to_user(row) - try: - pw_hash = decrypt(row["password_hash_enc"]) - except ValueError: - return None - return user, pw_hash - - -def find_user_by_id(user_id: str) -> Optional[User]: - with get_db() as conn: - row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() - if not row: - return None - return _row_to_user(row) - - -def get_totp_secret(user_id: str) -> Optional[str]: - with get_db() as conn: - row = conn.execute( - "SELECT totp_secret_enc FROM users WHERE id = ?", (user_id,) - ).fetchone() - if not row or not row["totp_secret_enc"]: - return None - try: - return decrypt(row["totp_secret_enc"]) - except ValueError: - return None - - -def list_users() -> list[User]: - with get_db() as conn: - rows = conn.execute( - "SELECT * FROM users ORDER BY created_at ASC" - ).fetchall() - return [_row_to_user(r) for r in rows] - - -def create_user(username: str, password: str, role: str = "user") -> User: - username = username.strip() - if not username or len(username) < 3: - raise ValueError("Username must be at least 3 characters") - if len(username) > 64: - raise ValueError("Username too long") - if role not in ("user", "admin"): - raise ValueError("Invalid role") - if len(password) < 8: - raise ValueError("Password must be at least 8 characters") - - uh = username_lookup_hash(username) - with get_db() as conn: - existing = conn.execute( - "SELECT id FROM users WHERE username_hash = ?", (uh,) - ).fetchone() - if existing: - raise ValueError("Username already taken") - - user_id = secrets.token_hex(16) - now = datetime.now(timezone.utc).isoformat() - pw_hash = hash_password(password) - - conn.execute( - """INSERT INTO users - (id, username_hash, username_enc, password_hash_enc, - totp_secret_enc, mfa_enabled, role, created_at, last_login) - VALUES (?, ?, ?, ?, NULL, 0, ?, ?, NULL)""", - (user_id, uh, encrypt(username), encrypt(pw_hash), role, now), - ) - conn.commit() - - return User( - id=user_id, username=username, role=role, mfa_enabled=False, - created_at=now, last_login=None, - ) - - -def delete_user(user_id: str) -> None: - with get_db() as conn: - # Also clean up any folder permission grants this user had - conn.execute("DELETE FROM folder_permissions WHERE user_id = ?", (user_id,)) - conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) - conn.commit() - - -def set_user_role(user_id: str, role: str) -> None: - if role not in ("user", "admin"): - raise ValueError("Invalid role") - with get_db() as conn: - conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id)) - conn.commit() - - -def update_password(user_id: str, new_password: str) -> None: - if len(new_password) < 8: - raise ValueError("Password must be at least 8 characters") - pw_hash = hash_password(new_password) - with get_db() as conn: - conn.execute( - "UPDATE users SET password_hash_enc = ? WHERE id = ?", - (encrypt(pw_hash), user_id), - ) - conn.commit() - - -def begin_mfa_enrollment(user_id: str) -> tuple[str, str]: - """Generate a new TOTP secret (not yet enabled) and return (secret, provisioning_uri).""" - user = find_user_by_id(user_id) - if not user: - raise ValueError("User not found") - secret = generate_totp_secret() - # Stash it but leave mfa_enabled = 0 until confirmed - with get_db() as conn: - conn.execute( - "UPDATE users SET totp_secret_enc = ?, mfa_enabled = 0 WHERE id = ?", - (encrypt(secret), user_id), - ) - conn.commit() - uri = totp_provisioning_uri(secret, user.username) - return secret, uri - - -def confirm_mfa_enrollment(user_id: str, code: str) -> bool: - secret = get_totp_secret(user_id) - if not secret or not verify_totp(secret, code): - return False - with get_db() as conn: - conn.execute("UPDATE users SET mfa_enabled = 1 WHERE id = ?", (user_id,)) - conn.commit() - return True - - -def disable_mfa(user_id: str) -> None: - with get_db() as conn: - conn.execute( - "UPDATE users SET mfa_enabled = 0, totp_secret_enc = NULL WHERE id = ?", - (user_id,), - ) - conn.commit() - - -def record_login(user_id: str) -> None: - with get_db() as conn: - conn.execute( - "UPDATE users SET last_login = ? WHERE id = ?", - (datetime.now(timezone.utc).isoformat(), user_id), - ) - conn.commit() - - -# --------------------------------------------------------------------------- -# JWT sessions -# --------------------------------------------------------------------------- -def create_session_token(user: User, scope: str = "app") -> str: - """scope='app' for main app, scope='admin' for admin panel.""" - now = datetime.now(timezone.utc) - payload = { - "sub": user.id, - "role": user.role, - "scope": scope, - "iat": int(now.timestamp()), - "exp": int((now + timedelta(seconds=SESSION_TTL_SECONDS)).timestamp()), - } - return jwt.encode(payload, _JWT_KEY, algorithm=JWT_ALGORITHM) - - -def verify_session_token(token: str, expected_scope: str) -> Optional[dict]: - try: - payload = jwt.decode(token, _JWT_KEY, algorithms=[JWT_ALGORITHM]) - if payload.get("scope") != expected_scope: - return None - return payload - except jwt.PyJWTError: - return None - - -# --------------------------------------------------------------------------- -# FastAPI dependencies -# --------------------------------------------------------------------------- -def _extract_token(request: Request, cookie_name: str) -> Optional[str]: - # Prefer cookie, fall back to Authorization header - token = request.cookies.get(cookie_name) - if token: - return token - auth = request.headers.get("authorization", "") - if auth.lower().startswith("bearer "): - return auth[7:].strip() - return None - - -def require_user(scope: str, cookie_name: str): - def _dep(request: Request) -> User: - token = _extract_token(request, cookie_name) - if not token: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated") - payload = verify_session_token(token, expected_scope=scope) - if not payload: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired session") - user = find_user_by_id(payload["sub"]) - if not user: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User no longer exists") - return user - return _dep - - -def require_admin(scope: str, cookie_name: str): - base = require_user(scope, cookie_name) - def _dep(request: Request) -> User: - user = base(request) - if not user.is_admin: - raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin role required") - return user - return _dep - - -# Pre-wired dependencies for the two app scopes -require_app_user = require_user("app", SESSION_COOKIE_NAME) -require_admin_user = require_admin("admin", ADMIN_SESSION_COOKIE_NAME) - -# App-scoped admin dependency: authenticates against the MAIN app session -# cookie (scope "app") but additionally requires the admin role. This lets the -# administration section run inside the main application (served at /admin) -# instead of as a separate process on its own port, while still being strictly -# limited to admin users. -require_app_admin = require_admin("app", SESSION_COOKIE_NAME) - - -# --------------------------------------------------------------------------- -# Simple rate limiter (per-IP, per-username) to slow brute-force attempts -# --------------------------------------------------------------------------- -class RateLimiter: - def __init__(self, window_seconds: int = 300, max_attempts: int = 10): - self.window = window_seconds - self.max = max_attempts - self._attempts: dict[str, list[float]] = {} - - def check(self, key: str) -> bool: - now = time.time() - cutoff = now - self.window - arr = [t for t in self._attempts.get(key, []) if t > cutoff] - if len(arr) >= self.max: - self._attempts[key] = arr - return False - return True - - def record(self, key: str) -> None: - now = time.time() - cutoff = now - self.window - arr = [t for t in self._attempts.get(key, []) if t > cutoff] - arr.append(now) - self._attempts[key] = arr - - def reset(self, key: str) -> None: - self._attempts.pop(key, None) - - -login_limiter = RateLimiter(window_seconds=300, max_attempts=10) diff --git a/Forensic-Mail-Indexer/backend/requirements.txt b/Forensic-Mail-Indexer/backend/requirements.txt deleted file mode 100644 index 9cbd286..0000000 --- a/Forensic-Mail-Indexer/backend/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -python-multipart==0.0.12 -libpff-python==20231205 -aiofiles==24.1.0 -argon2-cffi==23.1.0 -pyotp==2.9.0 -cryptography==43.0.1 -PyJWT==2.9.0 -qrcode[pil]==7.4.2 diff --git a/Forensic-Mail-Indexer/docker-compose.yml b/Forensic-Mail-Indexer/docker-compose.yml deleted file mode 100644 index d5504cc..0000000 --- a/Forensic-Mail-Indexer/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -services: - pst-indexer: - build: . - container_name: pst-indexer - ports: - - "8000:8000" # App + built-in administration (at /admin) - volumes: - - ./data:/app/data - restart: unless-stopped - environment: - - DATA_DIR=/app/data - # Set to "true" when serving over HTTPS - - COOKIE_SECURE=false - # Session lifetime in seconds (default 8 hours) - - SESSION_TTL_SECONDS=28800 - # Optional: provide your own master secret. If set, this becomes the - # root key for all encryption. If omitted, a random key is generated - # on first run and persisted in ./data/secret.key (chmod 600). - # - MASTER_SECRET=change-me-to-a-long-random-string diff --git a/Forensic-Mail-Indexer/entrypoint.sh b/Forensic-Mail-Indexer/entrypoint.sh deleted file mode 100644 index 8e6b790..0000000 --- a/Forensic-Mail-Indexer/entrypoint.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Start the application (port 8000). The administration section now runs inside -# this same app (served at /admin), so there is no longer a separate admin -# process or port. - -set -e - -cleanup() { - echo "Shutting down…" - kill -TERM "$MAIN_PID" 2>/dev/null || true - wait - exit 0 -} -trap cleanup SIGINT SIGTERM - -cd /app/backend - -echo "[start] App (with built-in administration) on :8000" -uvicorn app:app --host 0.0.0.0 --port 8000 --proxy-headers & -MAIN_PID=$! - -wait "$MAIN_PID" -EXIT_CODE=$? -echo "[exit] Service exited with code $EXIT_CODE" -exit $EXIT_CODE diff --git a/Forensic-Mail-Indexer/frontend/app.js b/Forensic-Mail-Indexer/frontend/app.js deleted file mode 100644 index 49cac5f..0000000 --- a/Forensic-Mail-Indexer/frontend/app.js +++ /dev/null @@ -1,877 +0,0 @@ -/* ============================================================ - PST Archive — frontend logic - - Setup wizard (first run) - - Login (+ MFA) - - Main app: upload, index, search, view - ============================================================ */ - -const API = "/api"; - -const state = { - activePstId: null, - activePstName: "", - folder: "", - query: "", - limit: 25, - offset: 0, - total: 0, - refreshTimer: null, - user: null, - folders: [], // [{id, name, file_count}, ...] - psts: [], // last loaded pst list - collapsedFolders: new Set(), // folder IDs (or "__uncat__") that are collapsed - movePstId: null, // currently being moved -}; - -const $ = (id) => document.getElementById(id); - -// ---------- helpers ---------- -function fmtBytes(n) { - if (!n) return "0 B"; - const units = ["B", "KB", "MB", "GB", "TB"]; - let i = 0; - while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } - return `${n.toFixed(i ? 1 : 0)} ${units[i]}`; -} - -function fmtDate(iso) { - if (!iso) return "—"; - try { - const d = new Date(iso); - if (isNaN(d)) return iso; - return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); - } catch { return iso; } -} - -function fmtDateTime(iso) { - if (!iso) return "—"; - try { - const d = new Date(iso); - if (isNaN(d)) return iso; - return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); - } catch { return iso; } -} - -function toast(msg, ms = 3000) { - const el = $("toast"); - el.textContent = msg; - el.classList.remove("hidden"); - clearTimeout(toast._t); - toast._t = setTimeout(() => el.classList.add("hidden"), ms); -} - -function escapeHtml(s) { - if (s == null) return ""; - return String(s) - .replace(/&/g, "&").replace(//g, ">") - .replace(/"/g, """).replace(/'/g, "'"); -} - -function renderSnippet(snippet) { - if (!snippet) return ""; - return escapeHtml(snippet) - .replace(/<mark>/g, "") - .replace(/<\/mark>/g, ""); -} - -function debounce(fn, ms) { - let t; - return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; -} - -// ---------- API ---------- -async function api(path, opts = {}) { - const res = await fetch(API + path, { - credentials: "same-origin", - ...opts, - }); - let body = null; - try { body = await res.json(); } catch {} - if (!res.ok) { - const err = new Error((body && body.detail) || res.statusText); - err.status = res.status; - err.body = body || {}; - throw err; - } - return body; -} - -// ---------- Screen switching ---------- -function showScreen(screen) { - ["setupScreen", "loginScreen", "mainApp"].forEach((id) => { - $(id).classList.toggle("hidden", id !== screen); - }); -} - -// ---------- Bootstrap ---------- -async function bootstrap() { - // 1. Try to fetch current user - try { - const { user } = await api("/me"); - state.user = user; - enterApp(); - return; - } catch (e) { - if (e.status !== 401) { - showLogin(); - toast("Connection problem — please retry."); - return; - } - } - - // 2. Not logged in — check setup status - try { - const { needs_setup } = await api("/setup/status"); - if (needs_setup) showSetup(); - else showLogin(); - } catch { - showLogin(); - } -} - -// ---------- Setup ---------- -function showSetup() { - showScreen("setupScreen"); - $("setupForm").reset(); - $("setupError").classList.add("hidden"); - setTimeout(() => $("setupForm").username?.focus(), 50); -} - -async function handleSetup(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("setupError"); - errEl.classList.add("hidden"); - - const username = form.username.value.trim(); - const password = form.password.value; - const password2 = form.password2.value; - - if (password !== password2) { - errEl.textContent = "Passwords do not match."; - errEl.classList.remove("hidden"); - return; - } - - try { - const { user } = await api("/setup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - }); - state.user = user; - toast("Welcome — admin account created."); - enterApp(); - } catch (err) { - errEl.textContent = err.message || "Setup failed."; - errEl.classList.remove("hidden"); - } -} - -// ---------- Login ---------- -function showLogin() { - showScreen("loginScreen"); - $("loginForm").reset(); - $("mfaField").classList.add("hidden"); - $("loginError").classList.add("hidden"); - setTimeout(() => $("loginForm").username?.focus(), 50); -} - -async function handleLogin(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("loginError"); - errEl.classList.add("hidden"); - - const payload = { - username: form.username.value.trim(), - password: form.password.value, - }; - const mfaInput = form.totp_code.value.trim(); - if (mfaInput) payload.totp_code = mfaInput; - - try { - const { user } = await api("/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - state.user = user; - toast(`Welcome, ${user.username}.`); - enterApp(); - } catch (err) { - if (err.status === 401 && err.body && err.body.mfa_required) { - $("mfaField").classList.remove("hidden"); - const f = $("loginForm").totp_code; - f.value = ""; - setTimeout(() => f.focus(), 50); - errEl.textContent = "Enter your authenticator code to continue."; - errEl.classList.remove("hidden"); - return; - } - errEl.textContent = err.message || "Login failed."; - errEl.classList.remove("hidden"); - } -} - -async function handleLogout() { - try { await api("/logout", { method: "POST" }); } catch {} - state.user = null; - state.activePstId = null; - resetReader(); - showLogin(); -} - -// ---------- Enter the app ---------- -function enterApp() { - showScreen("mainApp"); - $("userChip").textContent = `${state.user.username}${state.user.role === "admin" ? " · admin" : ""}`; - // Show the ADMINISTRATION button only for admins. - const adminBtn = $("adminNavBtn"); - if (adminBtn) adminBtn.classList.toggle("hidden", state.user.role !== "admin"); - checkHealth(); - loadPstList(); - setInterval(checkHealth, 30000); -} - -// ---------- Health ---------- -async function checkHealth() { - const dot = $("statusDot"); - const text = $("statusText"); - try { - await api("/health"); - dot.className = "status-dot ok"; - text.textContent = "connected"; - } catch { - dot.className = "status-dot err"; - text.textContent = "offline"; - } -} - -// ---------- PST list ---------- -async function loadPstList() { - try { - const [folders, items] = await Promise.all([ - api("/folders"), - api("/pst-files"), - ]); - state.folders = folders; - state.psts = items; - renderPstList(); - if (items.some((i) => i.status === "indexing")) { - clearTimeout(state.refreshTimer); - state.refreshTimer = setTimeout(loadPstList, 2500); - } - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Failed to load archives: ${e.message}`); - } -} - -function pstItemHtml(i) { - const active = i.id === state.activePstId ? "active" : ""; - const disabled = i.status !== "ready"; - const statusText = i.status === "indexing" ? "indexing…" : - i.status === "ready" ? `${i.message_count.toLocaleString()} msgs` : - i.status === "failed" ? "failed" : i.status; - return ` -
  • -
    - ${escapeHtml(i.original_name)} - -
    -
    - ${fmtBytes(i.size_bytes)} · ${fmtDate(i.uploaded_at)} - ${statusText} -
    -
  • - `; -} - -function renderPstList() { - const ul = $("pstList"); - const { folders, psts } = state; - - if (!folders.length && !psts.length) { - ul.innerHTML = '
  • No archives yet.
  • '; - return; - } - - // Group PSTs by folder_id - const byFolder = new Map(); - byFolder.set(null, []); // uncategorized bucket - folders.forEach((f) => byFolder.set(f.id, [])); - psts.forEach((p) => { - const bucket = byFolder.has(p.folder_id) ? p.folder_id : null; - byFolder.get(bucket).push(p); - }); - - const parts = []; - - // Render user-defined folders first (sorted alphabetically — server does this) - for (const f of folders) { - const items = byFolder.get(f.id) || []; - const collapsed = state.collapsedFolders.has(f.id) ? "collapsed" : ""; - parts.push(` -
  • -
    -
    - - ${escapeHtml(f.name)} - ${items.length} -
    - -
    -
      - ${items.length - ? items.map(pstItemHtml).join("") - : '
    • — empty —
    • '} -
    -
  • - `); - } - - // Uncategorized group — only shown if it has items OR there are no folders - const uncat = byFolder.get(null) || []; - if (uncat.length || !folders.length) { - const collapsed = state.collapsedFolders.has("__uncat__") ? "collapsed" : ""; - parts.push(` -
  • -
    -
    - - Uncategorized - ${uncat.length} -
    -
    -
      - ${uncat.length - ? uncat.map(pstItemHtml).join("") - : '
    • — empty —
    • '} -
    -
  • - `); - } - - ul.innerHTML = parts.join(""); - - // PST click - ul.querySelectorAll(".pst-item").forEach((el) => { - el.addEventListener("click", (e) => { - if (e.target.closest("[data-pst-menu]")) return; - if (el.dataset.disabled) { - toast(el.dataset.status === "indexing" ? "Still indexing — one moment…" : "This archive is unavailable."); - return; - } - selectPst(el.dataset.id, el.dataset.name); - }); - }); - - // PST action menu - ul.querySelectorAll("[data-pst-menu]").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - openPstMenu(e, btn.dataset.pstMenu); - }); - }); - - // Folder header toggle (collapse/expand) - ul.querySelectorAll("[data-folder-header]").forEach((el) => { - el.addEventListener("click", (e) => { - if (e.target.closest("[data-folder-menu]")) return; - const id = el.dataset.folderHeader; - if (state.collapsedFolders.has(id)) state.collapsedFolders.delete(id); - else state.collapsedFolders.add(id); - const group = el.closest(".folder-group"); - group.classList.toggle("collapsed"); - }); - }); - - // Folder action menu - ul.querySelectorAll("[data-folder-menu]").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - openFolderMenu(e, btn.dataset.folderMenu); - }); - }); -} - -// ---------- Context menus ---------- -function closeContextMenu() { - document.querySelectorAll(".context-menu").forEach((el) => el.remove()); -} - -function openContextMenu(x, y, items) { - closeContextMenu(); - const menu = document.createElement("div"); - menu.className = "context-menu"; - items.forEach((it) => { - if (it.divider) { - menu.appendChild(document.createElement("hr")); - return; - } - const btn = document.createElement("button"); - btn.textContent = it.label; - if (it.danger) btn.classList.add("danger"); - btn.addEventListener("click", () => { - closeContextMenu(); - it.onClick(); - }); - menu.appendChild(btn); - }); - document.body.appendChild(menu); - - // Position, keeping menu inside viewport - const rect = menu.getBoundingClientRect(); - const maxX = window.innerWidth - rect.width - 4; - const maxY = window.innerHeight - rect.height - 4; - menu.style.left = Math.min(x, maxX) + "px"; - menu.style.top = Math.min(y, maxY) + "px"; - - // Close on outside click or escape - setTimeout(() => { - document.addEventListener("click", closeContextMenu, { once: true }); - }, 0); -} - -function openPstMenu(event, pstId) { - const p = state.psts.find((x) => x.id === pstId); - if (!p) return; - const r = event.currentTarget.getBoundingClientRect(); - openContextMenu(r.left, r.bottom + 4, [ - { - label: "Move to folder…", - onClick: () => openMoveModal(p), - }, - { divider: true }, - { - label: "Delete archive", - danger: true, - onClick: () => deletePst(p), - }, - ]); -} - -function openFolderMenu(event, folderId) { - const f = state.folders.find((x) => x.id === folderId); - if (!f) return; - const r = event.currentTarget.getBoundingClientRect(); - openContextMenu(r.left, r.bottom + 4, [ - { - label: "Rename folder…", - onClick: () => renameFolder(f), - }, - { divider: true }, - { - label: "Delete folder", - danger: true, - onClick: () => deleteFolder(f), - }, - ]); -} - -// ---------- Folder operations ---------- -function openFolderModal() { - $("folderForm").reset(); - $("folderError").classList.add("hidden"); - $("folderModal").classList.remove("hidden"); - setTimeout(() => $("folderForm").name.focus(), 50); -} - -async function handleCreateFolder(e) { - e.preventDefault(); - const form = e.target; - const errEl = $("folderError"); - errEl.classList.add("hidden"); - try { - await api("/folders", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: form.name.value }), - }); - toast("Folder created."); - closeModals(); - loadPstList(); - } catch (err) { - errEl.textContent = err.message || "Create failed."; - errEl.classList.remove("hidden"); - } -} - -async function renameFolder(f) { - const newName = prompt(`Rename "${f.name}" to:`, f.name); - if (!newName || newName.trim() === f.name) return; - try { - await api(`/folders/${f.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: newName }), - }); - toast("Folder renamed."); - loadPstList(); - } catch (err) { - toast(`Rename failed: ${err.message}`); - } -} - -async function deleteFolder(f) { - if (!confirm(`Delete folder "${f.name}"?\n\nAny archives inside will be moved to Uncategorized.`)) return; - try { - await api(`/folders/${f.id}`, { method: "DELETE" }); - toast("Folder deleted."); - loadPstList(); - } catch (err) { - toast(`Delete failed: ${err.message}`); - } -} - -async function deletePst(p) { - if (!confirm(`Remove "${p.original_name}" and all indexed messages?`)) return; - try { - await api(`/pst-files/${p.id}`, { method: "DELETE" }); - if (state.activePstId === p.id) { state.activePstId = null; resetReader(); } - toast("Archive removed."); - loadPstList(); - } catch (err) { - toast(`Delete failed: ${err.message}`); - } -} - -function openMoveModal(p) { - state.movePstId = p.id; - $("moveSubject").textContent = p.original_name; - - const sel = $("moveFolderSelect"); - sel.innerHTML = '' + - state.folders.map((f) => - `` - ).join(""); - if (!p.folder_id) sel.value = ""; - - $("moveError").classList.add("hidden"); - $("moveModal").classList.remove("hidden"); -} - -async function handleMovePst(e) { - e.preventDefault(); - const errEl = $("moveError"); - errEl.classList.add("hidden"); - const folderId = $("moveFolderSelect").value || null; - try { - await api(`/pst-files/${state.movePstId}/folder`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folder_id: folderId }), - }); - toast("Moved."); - closeModals(); - loadPstList(); - } catch (err) { - errEl.textContent = err.message || "Move failed."; - errEl.classList.remove("hidden"); - } -} - -function closeModals() { - document.querySelectorAll(".modal").forEach((m) => m.classList.add("hidden")); -} - -// ---------- Selection + folders ---------- -async function selectPst(id, name) { - state.activePstId = id; - state.activePstName = name; - state.folder = ""; - state.query = ""; - state.offset = 0; - - $("searchInput").value = ""; - $("searchInput").disabled = false; - $("folderFilter").disabled = false; - - document.querySelectorAll(".pst-item").forEach((el) => { - el.classList.toggle("active", el.dataset.id === id); - }); - $("readerTitle").textContent = name; - $("readerMeta").textContent = "Loading messages…"; - hideMessageView(); - await Promise.all([loadFolders(), loadMessages()]); -} - -async function loadFolders() { - if (!state.activePstId) return; - try { - const folders = await api(`/pst-files/${state.activePstId}/folders`); - const sel = $("folderFilter"); - sel.innerHTML = - '' + - folders.map((f) => ``).join(""); - sel.value = state.folder || ""; - } catch (e) { console.warn("folders", e); } -} - -// ---------- Messages ---------- -async function loadMessages() { - if (!state.activePstId) return; - const params = new URLSearchParams(); - if (state.query) params.set("q", state.query); - if (state.folder) params.set("folder", state.folder); - params.set("limit", state.limit); - params.set("offset", state.offset); - - try { - const res = await api(`/pst-files/${state.activePstId}/messages?${params}`); - state.total = res.total; - renderMessages(res.items); - renderPaginator(); - $("readerMeta").textContent = - `${state.total.toLocaleString()} message${state.total === 1 ? "" : "s"}` + - (state.query ? ` matching "${state.query}"` : "") + - (state.folder ? ` in ${state.folder}` : ""); - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Search failed: ${e.message}`); - } -} - -function renderMessages(items) { - const list = $("messageList"); - if (!items.length) { - list.innerHTML = ` -
    -
    -

    No messages match.

    -
    `; - return; - } - list.innerHTML = items.map((m) => { - const hasAtt = m.attachments && m.attachments.trim().length > 0; - return ` -
    -
    ${escapeHtml(m.sender || "(unknown sender)")}
    -
    -
    ${escapeHtml(m.subject || "(no subject)")} ${hasAtt ? '' : ""}
    -
    ${renderSnippet(m.snippet || "")}
    -
    -
    -
    ${fmtDate(m.sent_date)}
    -
    ${escapeHtml((m.folder || "").split("/").filter(Boolean).pop() || "inbox")}
    -
    -
    `; - }).join(""); - list.querySelectorAll(".message-row").forEach((el) => { - el.addEventListener("click", () => openMessage(el.dataset.id)); - }); -} - -function renderPaginator() { - const p = $("paginator"); - if (state.total <= state.limit) { p.classList.add("hidden"); return; } - p.classList.remove("hidden"); - const from = state.offset + 1; - const to = Math.min(state.offset + state.limit, state.total); - $("pageInfo").textContent = `${from}–${to} of ${state.total.toLocaleString()}`; - $("prevBtn").disabled = state.offset === 0; - $("nextBtn").disabled = state.offset + state.limit >= state.total; -} - -// ---------- Message view ---------- -async function openMessage(id) { - try { - const m = await api(`/messages/${id}`); - $("mvSubject").textContent = m.subject || "(no subject)"; - $("mvSender").textContent = m.sender || "—"; - $("mvRecipients").textContent = m.recipients || "—"; - $("mvDate").textContent = fmtDateTime(m.sent_date); - $("mvFolder").textContent = m.folder || "—"; - if (m.attachments && m.attachments.trim()) { - $("mvAttLabel").classList.remove("hidden"); - $("mvAttachments").classList.remove("hidden"); - $("mvAttachments").textContent = m.attachments.split(" | ").join(", "); - } else { - $("mvAttLabel").classList.add("hidden"); - $("mvAttachments").classList.add("hidden"); - } - $("mvBody").textContent = m.body || "(empty body)"; - $("messageView").classList.remove("hidden"); - } catch (e) { - if (e.status === 401) { handleLogout(); return; } - toast(`Could not open message: ${e.message}`); - } -} - -function hideMessageView() { $("messageView").classList.add("hidden"); } - -function resetReader() { - $("readerTitle").textContent = "Select an archive"; - $("readerMeta").textContent = "Upload a PST file or select one from the left to begin."; - $("searchInput").disabled = true; - $("folderFilter").disabled = true; - $("messageList").innerHTML = ` -
    -
    -

    The archive awaits.

    -
    `; - $("paginator").classList.add("hidden"); - hideMessageView(); -} - -// ---------- Upload ---------- -function uploadFile(file) { - if (!file) return; - const lower = file.name.toLowerCase(); - if (!lower.endsWith(".pst") && !lower.endsWith(".ost")) { - toast("Only .pst and .ost files are accepted."); - return; - } - const progress = $("uploadProgress"); - const bar = $("progressBar"); - const pct = $("progressPct"); - const nm = $("progressName"); - progress.classList.remove("hidden"); - nm.textContent = file.name; - bar.style.width = "0%"; - pct.textContent = "0%"; - - const form = new FormData(); - form.append("file", file); - - const xhr = new XMLHttpRequest(); - xhr.open("POST", API + "/pst-files"); - xhr.withCredentials = true; - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - const p = Math.round((e.loaded / e.total) * 100); - bar.style.width = p + "%"; - pct.textContent = p + "%"; - } - }; - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - pct.textContent = "indexing…"; - bar.style.width = "100%"; - toast("Upload complete. Indexing started in background."); - setTimeout(() => progress.classList.add("hidden"), 2500); - loadPstList(); - } else if (xhr.status === 401) { - handleLogout(); - } else { - let msg = "Upload failed"; - try { msg = JSON.parse(xhr.responseText).detail || msg; } catch {} - toast(msg); - progress.classList.add("hidden"); - } - }; - xhr.onerror = () => { toast("Upload failed (network)."); progress.classList.add("hidden"); }; - xhr.send(form); -} - -// ---------- Theme ---------- -function initTheme() { - // Preference order: localStorage → prefers-color-scheme → light - let theme = "light"; - try { - const saved = localStorage.getItem("pst-theme"); - if (saved === "light" || saved === "dark") { - theme = saved; - } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { - theme = "dark"; - } - } catch { /* ignore storage errors */ } - document.documentElement.dataset.theme = theme; -} - -function toggleTheme() { - const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light"; - const next = current === "dark" ? "light" : "dark"; - document.documentElement.dataset.theme = next; - try { localStorage.setItem("pst-theme", next); } catch {} -} - -// Apply theme immediately so there's no flash of wrong theme -initTheme(); - -// ---------- Wire up ---------- -function init() { - $("setupForm").addEventListener("submit", handleSetup); - $("loginForm").addEventListener("submit", handleLogin); - $("logoutBtn").addEventListener("click", handleLogout); - - const adminNavBtn = $("adminNavBtn"); - if (adminNavBtn) { - adminNavBtn.addEventListener("click", () => { window.location.href = "/admin/"; }); - } - - // Theme toggles (both the in-masthead and floating variants) - ["themeToggle", "themeToggleFloat"].forEach((id) => { - const el = document.getElementById(id); - if (el) el.addEventListener("click", toggleTheme); - }); - - $("fileInput").addEventListener("change", (e) => { - const f = e.target.files[0]; - if (f) uploadFile(f); - e.target.value = ""; - }); - - const dz = $("dropzone"); - ["dragenter", "dragover"].forEach((ev) => { - dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("drag-over"); }); - }); - ["dragleave", "drop"].forEach((ev) => { - dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("drag-over"); }); - }); - dz.addEventListener("drop", (e) => { - const f = e.dataTransfer.files?.[0]; - if (f) uploadFile(f); - }); - - $("refreshBtn").addEventListener("click", loadPstList); - $("newFolderBtn").addEventListener("click", openFolderModal); - $("folderForm").addEventListener("submit", handleCreateFolder); - $("moveForm").addEventListener("submit", handleMovePst); - - // Modal close buttons + backdrop clicks + Escape - document.querySelectorAll("[data-close]").forEach((el) => { - el.addEventListener("click", closeModals); - }); - document.querySelectorAll(".modal").forEach((m) => { - m.addEventListener("click", (e) => { - if (e.target === m) closeModals(); - }); - }); - - const searchHandler = debounce(() => { - state.query = $("searchInput").value.trim(); - state.offset = 0; - loadMessages(); - }, 250); - $("searchInput").addEventListener("input", searchHandler); - - $("folderFilter").addEventListener("change", () => { - state.folder = $("folderFilter").value; - state.offset = 0; - loadMessages(); - }); - - $("prevBtn").addEventListener("click", () => { - state.offset = Math.max(0, state.offset - state.limit); - loadMessages(); - }); - $("nextBtn").addEventListener("click", () => { - state.offset += state.limit; - loadMessages(); - }); - - $("closeMessageBtn").addEventListener("click", hideMessageView); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - hideMessageView(); - closeModals(); - closeContextMenu(); - } - }); - - bootstrap(); -} - -document.addEventListener("DOMContentLoaded", init); diff --git a/Forensic-Mail-Indexer/frontend/index.html b/Forensic-Mail-Indexer/frontend/index.html deleted file mode 100644 index 76b9585..0000000 --- a/Forensic-Mail-Indexer/frontend/index.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - -PST Archive · Mail Indexer - - - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Forensic-Mail-Indexer/frontend/styles.css b/Forensic-Mail-Indexer/frontend/styles.css deleted file mode 100644 index 63db6c0..0000000 --- a/Forensic-Mail-Indexer/frontend/styles.css +++ /dev/null @@ -1,1182 +0,0 @@ -/* ============================================================ - PST ARCHIVE — aesthetic: editorial/forensic archive - Warm paper palette, serif display, mono accents, grain overlay - Supports light (default) and dark themes via [data-theme] - ============================================================ */ - -:root, -:root[data-theme="light"] { - --paper: #f2ede3; - --paper-deep: #e8e1d3; - --ink: #1b1814; - --ink-soft: #3a332a; - --ink-muted: #6b6258; - --rule: #c9bfad; - --rule-soft: #dcd3c1; - --accent: #a8341f; /* oxblood red */ - --accent-soft: #c95a42; - --highlight: #e8d67a; /* faded yellow marker */ - --ok: #2e6f3a; - --warn: #a86b1e; - --shadow: 0 1px 0 rgba(27, 24, 20, 0.04), 0 20px 40px -20px rgba(27, 24, 20, 0.25); - --grain-opacity: 0.35; - - --font-display: "Fraunces", "Iowan Old Style", "Palatino", Georgia, serif; - --font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, monospace; -} - -:root[data-theme="dark"] { - --paper: #17140f; /* deep inked paper */ - --paper-deep: #201c16; /* slightly lighter ink-pool for surfaces */ - --ink: #e9e0cb; /* warm paper-cream as the ink color */ - --ink-soft: #c4b99f; - --ink-muted: #8a8170; - --rule: #3a3226; - --rule-soft: #2a251d; - --accent: #d06347; /* softened oxblood that holds up on dark */ - --accent-soft: #e4876c; - --highlight: #d8b64a; /* brass-yellow marker */ - --ok: #6fae7c; - --warn: #d99a4a; - --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 20px 40px -20px rgba(0, 0, 0, 0.9); - --grain-opacity: 0.12; -} - -* { box-sizing: border-box; } - -html, body { - margin: 0; - padding: 0; - height: 100%; - background: var(--paper); - color: var(--ink); - font-family: var(--font-display); - font-feature-settings: "ss01", "onum"; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - display: flex; - flex-direction: column; - min-height: 100vh; - background-image: - radial-gradient(circle at 10% 10%, rgba(168, 52, 31, 0.04), transparent 40%), - radial-gradient(circle at 90% 90%, rgba(74, 58, 42, 0.05), transparent 50%); -} - -:root[data-theme="dark"] body { - background-image: - radial-gradient(circle at 10% 10%, rgba(208, 99, 71, 0.06), transparent 40%), - radial-gradient(circle at 90% 90%, rgba(216, 182, 74, 0.03), transparent 50%); -} - -/* grain overlay */ -.grain { - pointer-events: none; - position: fixed; - inset: 0; - z-index: 9999; - opacity: var(--grain-opacity); - mix-blend-mode: multiply; - background-image: url("data:image/svg+xml;utf8,"); -} - -:root[data-theme="dark"] .grain { - mix-blend-mode: screen; -} - -/* ============================================================ - MASTHEAD - ============================================================ */ -.masthead { - display: flex; - justify-content: space-between; - align-items: flex-end; - padding: 28px 40px 18px; - border-bottom: 1px solid var(--ink); - position: relative; -} - -.masthead::after { - content: ""; - position: absolute; - bottom: -4px; - left: 40px; - right: 40px; - height: 1px; - background: var(--ink); -} - -.masthead-left { display: flex; align-items: center; gap: 18px; } - -.logo-mark { - color: var(--ink); - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border: 1px solid var(--ink); - background: var(--paper); -} - -.masthead-title h1 { - margin: 0; - font-size: 2.5rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1; - font-variation-settings: "SOFT" 30, "WONK" 1; -} - -.tagline { - margin: 6px 0 0; - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.masthead-right { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--warn); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); -} -.status-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); } -.status-dot.err { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); } - -/* ============================================================ - WORKSPACE - ============================================================ */ -.workspace { - display: grid; - grid-template-columns: 340px 1fr; - flex: 1; - min-height: 0; -} - -/* ============================================================ - SIDEBAR - ============================================================ */ -.sidebar { - border-right: 1px solid var(--rule); - padding: 28px 24px; - display: flex; - flex-direction: column; - gap: 36px; - overflow-y: auto; - background: linear-gradient(180deg, rgba(232, 225, 211, 0.4), transparent 40%); -} - -.section-heading { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--ink-soft); - margin: 0 0 14px; - padding-bottom: 8px; - border-bottom: 1px solid var(--rule); - display: flex; - align-items: baseline; - justify-content: space-between; -} - -.dropzone { - display: block; - border: 1.5px dashed var(--ink-muted); - background: var(--paper-deep); - padding: 28px 18px; - text-align: center; - cursor: pointer; - transition: all 0.2s ease; - position: relative; -} -.dropzone:hover, -.dropzone.drag-over { - border-color: var(--accent); - background: var(--rule-soft); - transform: translateY(-1px); -} -.dropzone-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - color: var(--ink-soft); -} -.dropzone-title { - font-family: var(--font-display); - font-size: 1.1rem; - font-style: italic; - font-weight: 400; -} -.dropzone-title em { - font-style: normal; - font-family: var(--font-mono); - font-size: 0.9rem; - color: var(--accent); -} -.dropzone-hint { - font-family: var(--font-mono); - font-size: 0.65rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.upload-progress { margin-top: 14px; } -.progress-label { - display: flex; - justify-content: space-between; - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--ink-muted); - margin-bottom: 6px; -} -.progress-track { - height: 4px; - background: var(--rule-soft); - overflow: hidden; -} -.progress-bar { - height: 100%; - width: 0%; - background: var(--accent); - transition: width 0.2s ease; -} - -.archive-head { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 14px; -} -.archive-head .section-heading { - margin: 0; - border: none; - padding: 0; -} - -.icon-btn { - background: transparent; - border: 1px solid var(--rule); - padding: 5px 8px; - cursor: pointer; - color: var(--ink-soft); - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} -.icon-btn:hover { - border-color: var(--ink); - color: var(--accent); -} - -.pst-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; } - -.pst-item { - padding: 14px 14px; - border: 1px solid var(--rule); - background: var(--paper); - cursor: pointer; - transition: all 0.15s ease; - position: relative; -} -.pst-item:hover { - border-color: var(--ink); - transform: translateX(2px); -} -.pst-item.active { - border-color: var(--ink); - background: var(--ink); - color: var(--paper); - box-shadow: 4px 4px 0 var(--accent); -} -.pst-item.active .pst-meta { color: color-mix(in srgb, var(--paper) 65%, transparent); } - -.pst-name { - font-size: 1rem; - font-weight: 500; - letter-spacing: -0.01em; - margin: 0 0 6px; - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 8px; -} -.pst-name-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} -.pst-menu { - background: none; - border: none; - color: inherit; - opacity: 0.4; - cursor: pointer; - font-size: 1.1rem; - line-height: 1; - padding: 0 4px; - letter-spacing: 0.1em; - transition: opacity 0.15s, color 0.15s; -} -.pst-menu:hover { opacity: 1; color: var(--accent); } -.pst-item.active .pst-menu:hover { color: var(--accent-soft); } - -.pst-meta { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.08em; - color: var(--ink-muted); - display: flex; - justify-content: space-between; - gap: 8px; -} - -.pst-status { - text-transform: uppercase; - letter-spacing: 0.12em; -} -.pst-status.indexing { color: var(--warn); } -.pst-status.ready { color: var(--ok); } -.pst-status.failed { color: var(--accent); } -.pst-item.active .pst-status.indexing { color: color-mix(in srgb, var(--warn) 70%, var(--paper) 30%); } -.pst-item.active .pst-status.ready { color: color-mix(in srgb, var(--ok) 70%, var(--paper) 30%); } -.pst-item.active .pst-status.failed { color: var(--accent-soft); } - -.empty-state { - color: var(--ink-muted); - font-style: italic; - text-align: center; - padding: 20px 0; - font-size: 0.9rem; -} - -/* ============================================================ - FOLDER GROUPS IN SIDEBAR - ============================================================ */ -.archive-actions { - display: flex; - gap: 4px; -} - -.folder-group { - list-style: none; - padding: 0; - margin: 0; -} - -.folder-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 4px; - margin: 14px 0 6px; - cursor: pointer; - user-select: none; - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-soft); - border-bottom: 1px solid var(--rule); - transition: color 0.15s; -} -.folder-group:first-child .folder-header { margin-top: 4px; } - -.folder-header:hover { color: var(--accent); } - -.folder-header-left { - display: flex; - align-items: center; - gap: 6px; - overflow: hidden; -} - -.folder-caret { - display: inline-block; - width: 8px; - transition: transform 0.2s; - color: var(--ink-muted); -} -.folder-group.collapsed .folder-caret { transform: rotate(-90deg); } - -.folder-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.folder-count { - color: var(--ink-muted); - font-size: 0.65rem; - margin-left: 4px; -} - -.folder-menu-btn { - background: none; - border: none; - color: var(--ink-muted); - cursor: pointer; - padding: 2px 6px; - font-size: 0.9rem; - line-height: 1; - opacity: 0; - transition: opacity 0.15s, color 0.15s; -} -.folder-header:hover .folder-menu-btn { opacity: 1; } -.folder-menu-btn:hover { color: var(--accent); } - -.folder-items { - display: flex; - flex-direction: column; - gap: 8px; - overflow: hidden; - transition: max-height 0.25s ease-out; - max-height: 2000px; -} -.folder-group.collapsed .folder-items { - max-height: 0; -} - -.folder-empty { - font-style: italic; - font-size: 0.8rem; - color: var(--ink-muted); - padding: 6px 4px; -} - -/* Context menu for folder/PST actions */ -.context-menu { - position: fixed; - background: var(--paper); - border: 1px solid var(--ink); - box-shadow: 4px 4px 0 var(--rule); - z-index: 500; - min-width: 160px; - padding: 4px 0; -} -.context-menu button { - display: block; - width: 100%; - text-align: left; - background: none; - border: none; - padding: 8px 14px; - font-family: var(--font-display); - font-size: 0.9rem; - color: var(--ink); - cursor: pointer; -} -.context-menu button:hover { - background: var(--paper-deep); - color: var(--accent); -} -.context-menu button.danger:hover { - color: var(--accent); -} -.context-menu hr { - border: none; - border-top: 1px solid var(--rule-soft); - margin: 4px 0; -} - -/* ============================================================ - MODAL - ============================================================ */ -.modal { - position: fixed; - inset: 0; - background: color-mix(in srgb, var(--ink) 40%, transparent); - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - z-index: 200; - animation: fadeIn 0.2s ease-out; -} - -.modal-card { - background: var(--paper); - border: 1px solid var(--ink); - padding: 36px 36px 30px; - max-width: 420px; - width: 100%; - box-shadow: 8px 8px 0 var(--accent); - position: relative; -} -.modal-card h2 { - margin: 0 0 4px; - font-size: 1.5rem; - font-weight: 500; - letter-spacing: -0.015em; -} -.modal-subtitle { - margin: 0 0 24px; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--ink-muted); - letter-spacing: 0.06em; -} -.modal-close { - position: absolute; - top: 10px; - right: 14px; - background: none; - border: none; - font-size: 1.8rem; - line-height: 1; - cursor: pointer; - color: var(--ink-muted); - transition: color 0.15s, transform 0.15s; -} -.modal-close:hover { - color: var(--accent); - transform: rotate(90deg); -} - -.auth-label select { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 12px; - border: 1px solid var(--ink); - background: var(--paper-deep); - color: var(--ink); - outline: none; - cursor: pointer; -} -.auth-label select:focus { - background: var(--paper); - box-shadow: 3px 3px 0 var(--accent); -} - -/* ============================================================ - READER - ============================================================ */ -.reader { - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; -} - -.reader-head { - padding: 28px 40px 20px; - border-bottom: 1px solid var(--rule); - background: - linear-gradient(180deg, rgba(232, 225, 211, 0.3), transparent); -} - -.reader-title h2 { - margin: 0; - font-size: 2rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1.1; -} - -.reader-meta { - margin: 6px 0 20px; - font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.search-wrap { - display: grid; - grid-template-columns: 1fr 200px; - gap: 10px; -} - -#searchInput, #folderFilter { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 14px; - border: 1px solid var(--ink); - background: var(--paper); - color: var(--ink); - outline: none; - transition: box-shadow 0.15s; -} -#searchInput:focus, #folderFilter:focus { - box-shadow: 3px 3px 0 var(--accent); -} -#searchInput:disabled, #folderFilter:disabled { - border-color: var(--rule); - color: var(--ink-muted); - background: var(--paper-deep); - cursor: not-allowed; -} -#searchInput::placeholder { - color: var(--ink-muted); - font-style: italic; -} -#folderFilter { cursor: pointer; font-family: var(--font-mono); font-size: 0.8rem; } - -.reader-body { - flex: 1; - overflow-y: auto; - padding: 20px 40px; - position: relative; -} - -.reader-placeholder { - text-align: center; - padding: 80px 20px; - color: var(--ink-muted); -} -.placeholder-ornament { - font-size: 2rem; - color: var(--accent); - margin-bottom: 16px; -} -.reader-placeholder p { - font-style: italic; - font-size: 1.1rem; - margin: 0; -} - -/* ============================================================ - MESSAGE LIST - ============================================================ */ -.message-list { - display: flex; - flex-direction: column; -} - -.message-row { - padding: 16px 0; - border-bottom: 1px solid var(--rule-soft); - cursor: pointer; - transition: all 0.12s ease; - display: grid; - grid-template-columns: 180px 1fr 140px; - gap: 20px; - align-items: baseline; -} -.message-row:hover { - background: color-mix(in srgb, var(--paper-deep) 70%, transparent); - padding-left: 8px; - padding-right: 8px; -} - -.mr-sender { - font-weight: 500; - color: var(--ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.95rem; -} - -.mr-main { - min-width: 0; - overflow: hidden; -} -.mr-subject { - font-size: 1.05rem; - font-weight: 500; - margin: 0 0 4px; - color: var(--ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.mr-snippet { - font-size: 0.85rem; - color: var(--ink-muted); - line-height: 1.5; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; -} -.mr-snippet mark { - background: var(--highlight); - color: var(--ink); - padding: 0 2px; - font-weight: 500; -} - -.mr-meta { - text-align: right; - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--ink-muted); - letter-spacing: 0.04em; -} -.mr-folder { - margin-top: 4px; - font-size: 0.65rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--accent); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mr-attached { - display: inline-block; - margin-left: 4px; - color: var(--accent); -} - -/* ============================================================ - MESSAGE VIEW - ============================================================ */ -.message-view { - position: absolute; - inset: 20px 40px; - background: var(--paper); - border: 1px solid var(--ink); - box-shadow: 8px 8px 0 var(--rule); - overflow-y: auto; - padding: 40px 50px; - animation: slideIn 0.25s ease-out; -} - -@keyframes slideIn { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - -.close-btn { - position: absolute; - top: 14px; - right: 18px; - background: none; - border: none; - font-size: 2rem; - line-height: 1; - cursor: pointer; - color: var(--ink-muted); - transition: color 0.15s, transform 0.15s; -} -.close-btn:hover { - color: var(--accent); - transform: rotate(90deg); -} - -.message-header { - border-bottom: 2px solid var(--ink); - padding-bottom: 20px; - margin-bottom: 24px; -} - -.message-subject { - font-size: 1.75rem; - font-weight: 500; - line-height: 1.2; - letter-spacing: -0.015em; - margin-bottom: 16px; -} - -.message-meta { - display: grid; - grid-template-columns: 80px 1fr; - gap: 4px 20px; - margin: 0; -} -.message-meta dt { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); - padding-top: 2px; -} -.message-meta dd { - margin: 0; - font-size: 0.95rem; - color: var(--ink-soft); - word-break: break-word; -} - -.message-body { - font-family: var(--font-display); - font-size: 1rem; - line-height: 1.7; - color: var(--ink); - white-space: pre-wrap; - word-wrap: break-word; - max-width: 70ch; -} - -/* ============================================================ - PAGINATOR - ============================================================ */ -.paginator { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 40px; - border-top: 1px solid var(--rule); - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--ink-soft); - background: var(--paper-deep); -} -.paginator button { - background: none; - border: 1px solid var(--ink); - padding: 6px 14px; - cursor: pointer; - font-family: inherit; - font-size: inherit; - color: var(--ink); - transition: all 0.15s; -} -.paginator button:hover:not(:disabled) { - background: var(--ink); - color: var(--paper); -} -.paginator button:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -/* ============================================================ - TOAST - ============================================================ */ -.toast { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: var(--ink); - color: var(--paper); - padding: 12px 20px; - font-family: var(--font-mono); - font-size: 0.8rem; - letter-spacing: 0.06em; - border-left: 3px solid var(--accent); - box-shadow: var(--shadow); - z-index: 1000; - animation: toastSlide 0.3s ease-out; -} -@keyframes toastSlide { - from { opacity: 0; transform: translate(-50%, 20px); } - to { opacity: 1; transform: translate(-50%, 0); } -} - -.hidden { display: none !important; } - -/* ============================================================ - AUTH SCREENS (login + setup) - ============================================================ */ -.auth-screen { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - background: - radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 45%), - radial-gradient(circle at 70% 80%, color-mix(in srgb, var(--ink) 10%, transparent), transparent 55%), - var(--paper); - z-index: 100; - animation: fadeIn 0.3s ease-out; -} -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.auth-card { - width: 100%; - max-width: 440px; - background: var(--paper); - border: 1px solid var(--ink); - padding: 44px 40px 36px; - box-shadow: 8px 8px 0 var(--rule); - position: relative; -} -.auth-card::before { - content: ""; - position: absolute; - top: -6px; - left: -6px; - right: 6px; - bottom: 6px; - border: 1px solid var(--ink); - pointer-events: none; - z-index: -1; - background: var(--paper-deep); -} - -.auth-brand { - text-align: center; - margin-bottom: 28px; - padding-bottom: 20px; - border-bottom: 1px solid var(--rule); -} -.auth-brand svg { color: var(--ink); margin: 0 auto 12px; display: block; } -.auth-brand h1 { - margin: 0; - font-size: 2rem; - font-weight: 500; - letter-spacing: -0.02em; - line-height: 1; -} -.auth-tagline { - margin: 8px 0 0; - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--ink-muted); -} - -.auth-form { display: flex; flex-direction: column; gap: 16px; } - -.auth-intro { - margin: 0 0 6px; - font-size: 0.92rem; - color: var(--ink-soft); - line-height: 1.5; - font-style: italic; -} - -.auth-label { - display: flex; - flex-direction: column; - gap: 6px; -} -.auth-label > span { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--ink-soft); -} -.auth-label small { - font-family: var(--font-mono); - font-size: 0.66rem; - color: var(--ink-muted); - letter-spacing: 0.04em; -} -.auth-label input { - font-family: var(--font-display); - font-size: 1rem; - padding: 10px 12px; - border: 1px solid var(--ink); - background: var(--paper-deep); - color: var(--ink); - outline: none; - transition: box-shadow 0.15s, background 0.15s; -} -.auth-label input:focus { - background: var(--paper); - box-shadow: 3px 3px 0 var(--accent); -} - -.auth-btn { - margin-top: 8px; - padding: 12px 18px; - font-family: var(--font-display); - font-size: 1.05rem; - font-weight: 500; - letter-spacing: -0.01em; - background: var(--ink); - color: var(--paper); - border: 1px solid var(--ink); - cursor: pointer; - transition: all 0.15s; -} -.auth-btn:hover { - background: var(--accent); - border-color: var(--accent); - transform: translate(-1px, -1px); - box-shadow: 3px 3px 0 var(--ink); -} - -.auth-error { - padding: 10px 14px; - background: color-mix(in srgb, var(--accent) 10%, transparent); - border-left: 3px solid var(--accent); - color: var(--accent); - font-size: 0.88rem; - font-family: var(--font-mono); - letter-spacing: 0.02em; -} - -/* ============================================================ - USER CHIP in masthead - ============================================================ */ -.user-chip { - font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ink-soft); - padding: 4px 10px; - border: 1px solid var(--rule); - background: var(--paper-deep); - margin-left: 12px; -} - -.logout-btn { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-soft); - background: transparent; - border: 1px solid var(--rule); - padding: 5px 10px; - cursor: pointer; - transition: all 0.15s; - margin-left: 8px; -} -.logout-btn:hover { - border-color: var(--accent); - color: var(--accent); -} - -/* ADMINISTRATION button — shown next to the username for admin users only */ -.admin-nav-btn { - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--paper); - background: var(--accent); - border: 1px solid var(--accent); - padding: 5px 12px; - cursor: pointer; - transition: all 0.15s; - margin-left: 8px; - font-weight: 500; -} -.admin-nav-btn:hover { - background: var(--accent-soft); - border-color: var(--accent-soft); - transform: translateY(-1px); -} - -/* ============================================================ - SITE FOOTER (shown on the app view, not on login/setup) - ============================================================ */ -#mainApp:not(.hidden) { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.site-footer { - flex-shrink: 0; - padding: 14px 40px; - border-top: 1px solid var(--ink); - font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); - text-align: center; - background: var(--paper-deep); -} -@media (max-width: 900px) { - .site-footer { padding: 12px 20px; font-size: 0.62rem; } -} - -/* ============================================================ - THEME TOGGLE - ============================================================ */ -.theme-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--rule); - background: var(--paper-deep); - color: var(--ink-soft); - cursor: pointer; - padding: 0; - transition: all 0.2s ease; - position: relative; - overflow: hidden; -} -.theme-toggle:hover { - border-color: var(--accent); - color: var(--accent); - transform: translateY(-1px); -} -.theme-toggle svg { - width: 16px; - height: 16px; - transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.theme-toggle:hover svg { - transform: rotate(25deg); -} -:root[data-theme="dark"] .theme-toggle:hover svg { - transform: rotate(-25deg); -} - -/* Icon visibility — show sun in light mode, moon in dark mode */ -.theme-toggle .icon-sun { display: inline-block; } -.theme-toggle .icon-moon { display: none; } -:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; } -:root[data-theme="dark"] .theme-toggle .icon-moon { display: inline-block; } - -/* Floating variant shown on auth/setup screens */ -.theme-toggle-float { - position: fixed; - top: 20px; - right: 20px; - z-index: 200; -} -/* Hide the floating toggle once the main app is visible (in-masthead toggle takes over) */ -body:has(#mainApp:not(.hidden)) .theme-toggle-float { display: none; } - -/* ============================================================ - SCROLLBARS - ============================================================ */ -::-webkit-scrollbar { width: 10px; height: 10px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--rule); - border: 2px solid var(--paper); -} -::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); } - -/* ============================================================ - RESPONSIVE - ============================================================ */ -@media (max-width: 900px) { - .workspace { grid-template-columns: 1fr; } - .sidebar { border-right: none; border-bottom: 1px solid var(--rule); max-height: 50vh; } - .masthead { padding: 20px 24px 14px; flex-wrap: wrap; gap: 16px; } - .reader-head, .reader-body, .paginator { padding-left: 24px; padding-right: 24px; } - .search-wrap { grid-template-columns: 1fr; } - .message-row { grid-template-columns: 1fr; gap: 6px; } - .mr-meta { text-align: left; } - .message-view { inset: 12px; padding: 24px 20px; } - .message-meta { grid-template-columns: 60px 1fr; } -} diff --git a/ISDSS - Internal Survailance Data Storage System/v1.0/martinhal-datahub.zip b/ISDSS - Internal Survailance Data Storage System/v1.0/martinhal-datahub.zip deleted file mode 100644 index b2cff28..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.0/martinhal-datahub.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.1/martinhal-datahub.zip b/ISDSS - Internal Survailance Data Storage System/v1.1/martinhal-datahub.zip deleted file mode 100644 index 6bcea2c..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.1/martinhal-datahub.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.2/martinhal-datahub.zip b/ISDSS - Internal Survailance Data Storage System/v1.2/martinhal-datahub.zip deleted file mode 100644 index 43ced28..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.2/martinhal-datahub.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.1/martinhal-isdss-v1.3-patch0.1.zip b/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.1/martinhal-isdss-v1.3-patch0.1.zip deleted file mode 100644 index 7567b73..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.1/martinhal-isdss-v1.3-patch0.1.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.2/martinhal-isdss-v1.3-patch0.2.zip b/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.2/martinhal-isdss-v1.3-patch0.2.zip deleted file mode 100644 index 03ee395..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.2/martinhal-isdss-v1.3-patch0.2.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.3/martinhal-isdss-v1.3-patch0.3.zip b/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.3/martinhal-isdss-v1.3-patch0.3.zip deleted file mode 100644 index 620b804..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.3/martinhal-isdss-v1.3-patch0.3.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.4/martinhal-isdss-v1.3-patch0.4.zip b/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.4/martinhal-isdss-v1.3-patch0.4.zip deleted file mode 100644 index e586dcf..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.3 Patch 0.4/martinhal-isdss-v1.3-patch0.4.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.3/martinhal-isdss-v1.3.zip b/ISDSS - Internal Survailance Data Storage System/v1.3/martinhal-isdss-v1.3.zip deleted file mode 100644 index 5fdf476..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.3/martinhal-isdss-v1.3.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.1/martinhal-isdss-v1.4-patch0.1.zip b/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.1/martinhal-isdss-v1.4-patch0.1.zip deleted file mode 100644 index 4fc0c3a..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.1/martinhal-isdss-v1.4-patch0.1.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.2/martinhal-isdss-v1.4-patch0.2.zip b/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.2/martinhal-isdss-v1.4-patch0.2.zip deleted file mode 100644 index a9a9b3d..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.2/martinhal-isdss-v1.4-patch0.2.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.3/martinhal-isdss-v1.4-patch0.3.zip b/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.3/martinhal-isdss-v1.4-patch0.3.zip deleted file mode 100644 index 3b37b3f..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.4 Patch 0.3/martinhal-isdss-v1.4-patch0.3.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.1/martinhal-isdss-v1.5-patch0.1.zip b/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.1/martinhal-isdss-v1.5-patch0.1.zip deleted file mode 100644 index 7819eba..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.1/martinhal-isdss-v1.5-patch0.1.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.2/martinhal-isdss-v1.5-patch0.2.zip b/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.2/martinhal-isdss-v1.5-patch0.2.zip deleted file mode 100644 index b250070..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.5 Patch 0.2/martinhal-isdss-v1.5-patch0.2.zip and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.5/ISDSS Logo.png b/ISDSS - Internal Survailance Data Storage System/v1.5/ISDSS Logo.png deleted file mode 100644 index 0e1f31b..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.5/ISDSS Logo.png and /dev/null differ diff --git a/ISDSS - Internal Survailance Data Storage System/v1.5/martinhal-isdss-v1.5.zip b/ISDSS - Internal Survailance Data Storage System/v1.5/martinhal-isdss-v1.5.zip deleted file mode 100644 index dc664af..0000000 Binary files a/ISDSS - Internal Survailance Data Storage System/v1.5/martinhal-isdss-v1.5.zip and /dev/null differ diff --git a/Jellyfin b/Jellyfin deleted file mode 160000 index 6bc3bee..0000000 --- a/Jellyfin +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6bc3bee00e5a84d0952dba4eea36f0f50cf44c18 diff --git a/Infosec/MEDIA/InfosecLogo.png b/MEDIA/InfosecLogo.png similarity index 100% rename from Infosec/MEDIA/InfosecLogo.png rename to MEDIA/InfosecLogo.png diff --git a/Martinhal Birthday Mailing App/v1.0/birthday-app.zip b/Martinhal Birthday Mailing App/v1.0/birthday-app.zip deleted file mode 100644 index a6528e7..0000000 Binary files a/Martinhal Birthday Mailing App/v1.0/birthday-app.zip and /dev/null differ diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app.zip b/Martinhal Birthday Mailing App/v1.1/birthday-app.zip deleted file mode 100644 index 87a1e39..0000000 Binary files a/Martinhal Birthday Mailing App/v1.1/birthday-app.zip and /dev/null differ diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/Dockerfile b/Martinhal Birthday Mailing App/v1.1/birthday-app/Dockerfile deleted file mode 100644 index 390c06a..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATA_DIR=/data PORT=8080 -VOLUME /data -EXPOSE 8080 -CMD ["python", "app.py"] diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/README.md b/Martinhal Birthday Mailing App/v1.1/birthday-app/README.md deleted file mode 100644 index 94b77da..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Birthday Manager - -Self-hosted birthday reminder application that automatically e-mails people on their birthday. - -© 2026 Martinhal IT - Joao Vaz - Version 1.1 - -## Features - -**Page 1 — Dashboard** -- Banner showing whose birthday it is today (based on system date/time) with a **Force Send** button to resend the e-mail -- Add / remove birthday entries: First Name, Last Name, Date of Birth, Gender, E-mail address -- Yearly calendar overview of all entries (hover a marked day to see the names; browse other years) - -**Page 2 — Administration** -- Add / remove application users -- Every user can enable/disable MFA (TOTP — Google Authenticator, Authy, etc.) and change their own password -- Full e-mail log: who received mail, at what date & time, and whether it succeeded -- Two template zones: **E-mail Template Male** and **E-Mail Template Female**, with placeholders `{first_name}`, `{last_name}`, `{age}` -- Image upload — reference an uploaded image inside a template with `` and it is embedded inline in the outgoing e-mail - -Automatic sending: a scheduler runs every day at `SEND_HOUR` (default 08:00) and e-mails everyone whose birthday it is, using the template matching their gender. Duplicates are avoided (one automatic mail per person per day); **Force Send** always resends. February 29 birthdays are celebrated on February 28 in non-leap years. - -Every successfully sent e-mail is also copied to the mailbox's **Sent** folder via IMAP, so it shows up in your normal mail client. - -## Mail provider - -The app comes pre-configured for **Purelymail**: - -| | Server | Port | Security | -|---|---|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com` | `465` | SSL/TLS | -| Incoming (IMAP, Sent-folder copy) | `imap.purelymail.com` | `993` | SSL/TLS | - -You only need to fill in `SMTP_USER`, `SMTP_PASS` and `SMTP_FROM` with your Purelymail mailbox credentials. If your network only supports STARTTLS for SMTP, set `SMTP_PORT=587` and `SMTP_SECURITY=starttls` instead. Any other provider works too — just override the host/port variables. - -## Quick start (Docker — recommended) - -1. Edit `docker-compose.yml` and fill in your Purelymail credentials (`SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`) plus a random `SECRET_KEY` and your timezone. -2. Run: - -```bash -docker compose up -d --build -``` - -3. Open http://localhost:8080 and log in with **admin / admin** — then change the password on the Administration page immediately. - -All data (SQLite database + uploaded images) lives in `./data`, so back up that folder. - -## Quick start (plain Python) - -```bash -pip install -r requirements.txt - -# Purelymail servers are the built-in defaults - only credentials are needed -export SMTP_USER=you@yourdomain.com -export SMTP_PASS=your-purelymail-password -export SMTP_FROM=you@yourdomain.com -export SEND_HOUR=8 -export SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))") - -python app.py -``` - -The app listens on port 8080 (`PORT` env var to change). Data is stored in `./data` (`DATA_DIR` env var to change). - -## Environment variables - -| Variable | Default | Purpose | -|---|---|---| -| `SMTP_HOST` | `smtp.purelymail.com` | SMTP server | -| `SMTP_PORT` | `465` | SMTP port | -| `SMTP_USER` | – | SMTP username / mailbox | -| `SMTP_PASS` | – | SMTP password | -| `SMTP_FROM` | `SMTP_USER` | From address | -| `SMTP_SECURITY` | `ssl` | `ssl`, `starttls` or `none` | -| `IMAP_HOST` | `imap.purelymail.com` | IMAP server for the Sent-folder copy | -| `IMAP_PORT` | `993` | IMAP port (SSL/TLS) | -| `IMAP_USER` | `SMTP_USER` | IMAP username | -| `IMAP_PASS` | `SMTP_PASS` | IMAP password | -| `IMAP_SENT_FOLDER` | `Sent` | Mailbox folder sent mail is stored in | -| `SEND_HOUR` | `8` | Hour (0–23) the daily automatic send runs | -| `SECRET_KEY` | random | Flask session key — set it so logins survive restarts | -| `PORT` | `8080` | HTTP port | -| `DATA_DIR` | `./data` | Where the database and uploads are stored | - -## Notes - -- The e-mail address field on entries is required because it is where the birthday greeting is sent. -- Failed sends (e.g. SMTP misconfigured) are also recorded in the e-mail log with the error as tooltip on the "Failed" status. If a mail is delivered but the IMAP copy to the Sent folder fails, the mail is still logged as Sent and the IMAP error appears as a tooltip. -- Run behind a reverse proxy (nginx / Caddy / Traefik) with HTTPS if you expose it beyond your LAN. diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/app.py b/Martinhal Birthday Mailing App/v1.1/birthday-app/app.py deleted file mode 100644 index f39cb8e..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/app.py +++ /dev/null @@ -1,647 +0,0 @@ -""" -Birthday Manager - self-hosted birthday reminder & e-mail application -(c) 2026 Martinhal IT - Joao Vaz - Version 1.1 -""" -import base64 -import calendar as cal -import imaplib -import io -import os -import re -import secrets -import smtplib -import sqlite3 -import uuid -from datetime import date, datetime -from email.mime.image import MIMEImage -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from functools import wraps - -import pyotp -import qrcode -from apscheduler.schedulers.background import BackgroundScheduler -from flask import (Flask, flash, g, redirect, render_template, request, - send_from_directory, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -DATA_DIR = os.environ.get("DATA_DIR", os.path.join(BASE_DIR, "data")) -UPLOAD_DIR = os.path.join(DATA_DIR, "uploads") -DB_PATH = os.path.join(DATA_DIR, "birthday.db") -ALLOWED_IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp"} - -os.makedirs(UPLOAD_DIR, exist_ok=True) - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32)) -app.config["MAX_CONTENT_LENGTH"] = 8 * 1024 * 1024 # 8 MB uploads - -APP_VERSION = "Version 1.1" -FOOTER_TEXT = "\u00a9 2026 Martinhal IT - Joao Vaz - " + APP_VERSION - -# ---------------------------------------------------------------- database - -def get_db(): - if "db" not in g: - g.db = sqlite3.connect(DB_PATH) - g.db.row_factory = sqlite3.Row - g.db.execute("PRAGMA foreign_keys = ON") - return g.db - - -@app.teardown_appcontext -def close_db(_exc): - db = g.pop("db", None) - if db is not None: - db.close() - - -def db_connect(): - """Standalone connection for use outside a request (scheduler).""" - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn - - -DEFAULT_TEMPLATE_MALE = ( - "

    Dear {first_name} {last_name},

    " - "

    Happy Birthday! Wishing you a fantastic day and a wonderful year ahead. " - "Congratulations on turning {age}!

    " - "

    Best regards,
    Martinhal IT

    " -) -DEFAULT_TEMPLATE_FEMALE = ( - "

    Dear {first_name} {last_name},

    " - "

    Happy Birthday! Wishing you a beautiful day filled with joy. " - "Congratulations on turning {age}!

    " - "

    Warm regards,
    Martinhal IT

    " -) - - -def init_db(): - conn = db_connect() - conn.executescript( - """ - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - mfa_secret TEXT, - mfa_enabled INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS birthdays ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - dob TEXT NOT NULL, -- YYYY-MM-DD - gender TEXT NOT NULL, -- Male / Female - email TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS email_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - recipient_name TEXT NOT NULL, - recipient_email TEXT NOT NULL, - subject TEXT NOT NULL, - sent_at TEXT NOT NULL, -- YYYY-MM-DD HH:MM:SS - status TEXT NOT NULL, -- Sent / Failed - detail TEXT - ); - CREATE TABLE IF NOT EXISTS templates ( - gender TEXT PRIMARY KEY, -- Male / Female - subject TEXT NOT NULL, - body TEXT NOT NULL - ); - """ - ) - # Seed default admin user - if conn.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - conn.execute( - "INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)", - ("admin", generate_password_hash("admin"), - datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ) - print(" * Created default user 'admin' with password 'admin' - change it!") - # Seed default templates - for gender, body in (("Male", DEFAULT_TEMPLATE_MALE), - ("Female", DEFAULT_TEMPLATE_FEMALE)): - conn.execute( - "INSERT OR IGNORE INTO templates (gender, subject, body) VALUES (?,?,?)", - (gender, "Happy Birthday, {first_name}!", body), - ) - conn.commit() - conn.close() - - -# ---------------------------------------------------------------- auth - -def login_required(view): - @wraps(view) - def wrapped(*args, **kwargs): - if not session.get("user_id"): - return redirect(url_for("login", next=request.path)) - return view(*args, **kwargs) - return wrapped - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - username = request.form.get("username", "").strip() - password = request.form.get("password", "") - user = get_db().execute( - "SELECT * FROM users WHERE username = ?", (username,) - ).fetchone() - if user and check_password_hash(user["password_hash"], password): - if user["mfa_enabled"]: - session.clear() - session["mfa_pending_user"] = user["id"] - return redirect(url_for("mfa_verify")) - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - return redirect(url_for("dashboard")) - flash("Invalid username or password.", "error") - return render_template("login.html", footer=FOOTER_TEXT) - - -@app.route("/mfa-verify", methods=["GET", "POST"]) -def mfa_verify(): - pending = session.get("mfa_pending_user") - if not pending: - return redirect(url_for("login")) - if request.method == "POST": - code = request.form.get("code", "").strip().replace(" ", "") - user = get_db().execute("SELECT * FROM users WHERE id = ?", (pending,)).fetchone() - if user and user["mfa_secret"] and pyotp.TOTP(user["mfa_secret"]).verify(code, valid_window=1): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - return redirect(url_for("dashboard")) - flash("Invalid authentication code.", "error") - return render_template("mfa_verify.html", footer=FOOTER_TEXT) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- helpers - -def calc_age(dob: date, on: date) -> int: - years = on.year - dob.year - if (on.month, on.day) < (dob.month, dob.day): - years -= 1 - return years - - -def birthday_matches_today(dob: date, today: date) -> bool: - """Feb 29 birthdays are celebrated on Feb 28 in non-leap years.""" - if dob.month == 2 and dob.day == 29 and not cal.isleap(today.year): - return today.month == 2 and today.day == 28 - return dob.month == today.month and dob.day == today.day - - -def todays_birthdays(conn): - today = date.today() - rows = conn.execute("SELECT * FROM birthdays").fetchall() - result = [] - for r in rows: - dob = datetime.strptime(r["dob"], "%Y-%m-%d").date() - if birthday_matches_today(dob, today): - result.append((r, calc_age(dob, today))) - return result - - -def render_placeholders(text: str, row, age: int) -> str: - return (text.replace("{first_name}", row["first_name"]) - .replace("{last_name}", row["last_name"]) - .replace("{age}", str(age))) - - -def smtp_settings(): - # Defaults are pre-configured for Purelymail (smtp.purelymail.com:465 SSL/TLS). - # If your client only supports STARTTLS, set SMTP_PORT=587 and SMTP_SECURITY=starttls. - return { - "host": os.environ.get("SMTP_HOST", "smtp.purelymail.com"), - "port": int(os.environ.get("SMTP_PORT", "465")), - "user": os.environ.get("SMTP_USER", ""), - "password": os.environ.get("SMTP_PASS", ""), - "sender": os.environ.get("SMTP_FROM", os.environ.get("SMTP_USER", "birthday@localhost")), - "security": os.environ.get("SMTP_SECURITY", "ssl").lower(), # ssl / starttls / none - } - - -def imap_settings(): - # Defaults are pre-configured for Purelymail (imap.purelymail.com:993 SSL/TLS). - # IMAP credentials fall back to the SMTP ones if not set separately. - return { - "host": os.environ.get("IMAP_HOST", "imap.purelymail.com"), - "port": int(os.environ.get("IMAP_PORT", "993")), - "user": os.environ.get("IMAP_USER", os.environ.get("SMTP_USER", "")), - "password": os.environ.get("IMAP_PASS", os.environ.get("SMTP_PASS", "")), - "folder": os.environ.get("IMAP_SENT_FOLDER", "Sent"), - } - - -def save_to_sent_folder(msg) -> str: - """Append the sent message to the mailbox's Sent folder via IMAP. - Returns '' on success or an error description on failure.""" - cfg = imap_settings() - if not cfg["user"]: - return "IMAP not configured - message not copied to Sent folder" - try: - imap = imaplib.IMAP4_SSL(cfg["host"], cfg["port"], timeout=20) - imap.login(cfg["user"], cfg["password"]) - folder = cfg["folder"] - # Create the folder if the server doesn't have it yet - status, _ = imap.select(folder) - if status != "OK": - imap.create(folder) - imap.append(folder, r"(\Seen)", imaplib.Time2Internaldate(datetime.now().timestamp()), - msg.as_bytes()) - imap.logout() - return "" - except Exception as exc: - return f"Delivered, but copy to Sent folder failed: {exc}" - - -IMG_SRC_RE = re.compile(r'src="/uploads/([^"]+)"') - - -def build_email(row, age: int, conn) -> MIMEMultipart: - tpl = conn.execute( - "SELECT * FROM templates WHERE gender = ?", (row["gender"],) - ).fetchone() - subject = render_placeholders(tpl["subject"], row, age) - body_html = render_placeholders(tpl["body"], row, age) - - msg = MIMEMultipart("related") - msg["Subject"] = subject - msg["From"] = smtp_settings()["sender"] - msg["To"] = row["email"] - - # Embed uploaded images referenced in the template as inline attachments - inline = [] - def to_cid(match): - fname = match.group(1) - path = os.path.join(UPLOAD_DIR, secure_filename(fname)) - if os.path.isfile(path): - cid = uuid.uuid4().hex - inline.append((path, cid)) - return f'src="cid:{cid}"' - return match.group(0) - - body_html = IMG_SRC_RE.sub(to_cid, body_html) - - alt = MIMEMultipart("alternative") - alt.attach(MIMEText(re.sub(r"<[^>]+>", " ", body_html), "plain", "utf-8")) - alt.attach(MIMEText(body_html, "html", "utf-8")) - msg.attach(alt) - - for path, cid in inline: - with open(path, "rb") as f: - img = MIMEImage(f.read()) - img.add_header("Content-ID", f"<{cid}>") - img.add_header("Content-Disposition", "inline", filename=os.path.basename(path)) - msg.attach(img) - return msg - - -def send_birthday_email(row, age: int, conn) -> None: - cfg = smtp_settings() - msg = build_email(row, age, conn) - status, detail = "Sent", "" - try: - if not cfg["host"]: - raise RuntimeError("SMTP is not configured (set SMTP_HOST etc.)") - if cfg["security"] == "ssl": - server = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=20) - else: - server = smtplib.SMTP(cfg["host"], cfg["port"], timeout=20) - if cfg["security"] == "starttls": - server.starttls() - if cfg["user"]: - server.login(cfg["user"], cfg["password"]) - server.send_message(msg) - server.quit() - detail = save_to_sent_folder(msg) # keep a copy in the mailbox Sent folder - except Exception as exc: # log failures too - status, detail = "Failed", str(exc) - conn.execute( - "INSERT INTO email_log (recipient_name, recipient_email, subject, sent_at, status, detail)" - " VALUES (?,?,?,?,?,?)", - (f'{row["first_name"]} {row["last_name"]}', row["email"], msg["Subject"], - datetime.now().strftime("%Y-%m-%d %H:%M:%S"), status, detail), - ) - conn.commit() - - -def already_sent_today(conn, row) -> bool: - today = date.today().strftime("%Y-%m-%d") - hit = conn.execute( - "SELECT 1 FROM email_log WHERE recipient_email = ? AND status = 'Sent'" - " AND sent_at LIKE ?", (row["email"], today + "%"), - ).fetchone() - return hit is not None - - -def daily_birthday_job(): - conn = db_connect() - try: - for row, age in todays_birthdays(conn): - if not already_sent_today(conn, row): - send_birthday_email(row, age, conn) - finally: - conn.close() - - -# ---------------------------------------------------------------- page 1 - -MONTH_NAMES = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"] - - -def build_year_calendar(conn, year: int): - """Return list of month dicts with week grids; birthday days carry names.""" - rows = conn.execute("SELECT * FROM birthdays").fetchall() - marks = {} - for r in rows: - dob = datetime.strptime(r["dob"], "%Y-%m-%d").date() - m, d = dob.month, dob.day - if m == 2 and d == 29 and not cal.isleap(year): - d = 28 - marks.setdefault((m, d), []).append(f'{r["first_name"]} {r["last_name"]}') - months = [] - c = cal.Calendar(firstweekday=0) # Monday - today = date.today() - for m in range(1, 13): - weeks = [] - for week in c.monthdayscalendar(year, m): - weeks.append([ - { - "day": d or "", - "names": marks.get((m, d), []) if d else [], - "today": bool(d) and today.year == year and today.month == m and today.day == d, - } for d in week - ]) - months.append({"name": MONTH_NAMES[m - 1], "weeks": weeks}) - return months - - -@app.route("/", methods=["GET"]) -@login_required -def dashboard(): - conn = get_db() - year = request.args.get("year", type=int) or date.today().year - entries = conn.execute( - "SELECT * FROM birthdays ORDER BY substr(dob,6) ASC" - ).fetchall() - today_list = [ - {"row": r, "age": a, "sent": already_sent_today(conn, r)} - for r, a in todays_birthdays(conn) - ] - return render_template( - "dashboard.html", - footer=FOOTER_TEXT, - username=session.get("username"), - entries=entries, - today_list=today_list, - today=date.today(), - months=build_year_calendar(conn, year), - year=year, - ) - - -@app.route("/birthdays/add", methods=["POST"]) -@login_required -def add_birthday(): - f = request.form - first = f.get("first_name", "").strip() - last = f.get("last_name", "").strip() - dob = f.get("dob", "").strip() - gender = f.get("gender", "").strip() - email = f.get("email", "").strip() - try: - datetime.strptime(dob, "%Y-%m-%d") - assert first and last and email and gender in ("Male", "Female") - except (ValueError, AssertionError): - flash("Please fill in all fields with valid values.", "error") - return redirect(url_for("dashboard")) - get_db().execute( - "INSERT INTO birthdays (first_name, last_name, dob, gender, email) VALUES (?,?,?,?,?)", - (first, last, dob, gender, email), - ) - get_db().commit() - flash(f"Added {first} {last}.", "ok") - return redirect(url_for("dashboard")) - - -@app.route("/birthdays//delete", methods=["POST"]) -@login_required -def delete_birthday(bid): - get_db().execute("DELETE FROM birthdays WHERE id = ?", (bid,)) - get_db().commit() - flash("Entry removed.", "ok") - return redirect(url_for("dashboard")) - - -@app.route("/force-send", methods=["POST"]) -@login_required -def force_send(): - conn = get_db() - todays = todays_birthdays(conn) - if not todays: - flash("Nobody has a birthday today - nothing to send.", "error") - else: - for row, age in todays: - send_birthday_email(row, age, conn) - flash(f"Birthday e-mail resent to {len(todays)} recipient(s). Check the log on the Administration page.", "ok") - return redirect(url_for("dashboard")) - - -# ---------------------------------------------------------------- page 2 - -@app.route("/admin") -@login_required -def admin(): - conn = get_db() - users = conn.execute("SELECT id, username, mfa_enabled, created_at FROM users ORDER BY username").fetchall() - log = conn.execute("SELECT * FROM email_log ORDER BY sent_at DESC, id DESC LIMIT 500").fetchall() - tpl_male = conn.execute("SELECT * FROM templates WHERE gender='Male'").fetchone() - tpl_female = conn.execute("SELECT * FROM templates WHERE gender='Female'").fetchone() - images = sorted(os.listdir(UPLOAD_DIR)) - return render_template( - "admin.html", - footer=FOOTER_TEXT, - username=session.get("username"), - user_id=session.get("user_id"), - users=users, - log=log, - tpl_male=tpl_male, - tpl_female=tpl_female, - images=images, - ) - - -@app.route("/users/add", methods=["POST"]) -@login_required -def add_user(): - username = request.form.get("username", "").strip() - password = request.form.get("password", "") - if not username or len(password) < 4: - flash("Username and a password of at least 4 characters are required.", "error") - return redirect(url_for("admin")) - try: - get_db().execute( - "INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)", - (username, generate_password_hash(password), - datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ) - get_db().commit() - flash(f"User '{username}' created.", "ok") - except sqlite3.IntegrityError: - flash("That username already exists.", "error") - return redirect(url_for("admin")) - - -@app.route("/users//delete", methods=["POST"]) -@login_required -def delete_user(uid): - conn = get_db() - if uid == session.get("user_id"): - flash("You cannot delete the account you are logged in with.", "error") - elif conn.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] <= 1: - flash("At least one user must remain.", "error") - else: - conn.execute("DELETE FROM users WHERE id = ?", (uid,)) - conn.commit() - flash("User removed.", "ok") - return redirect(url_for("admin")) - - -@app.route("/account/password", methods=["POST"]) -@login_required -def change_password(): - current = request.form.get("current", "") - new = request.form.get("new", "") - confirm = request.form.get("confirm", "") - conn = get_db() - user = conn.execute("SELECT * FROM users WHERE id = ?", (session["user_id"],)).fetchone() - if not check_password_hash(user["password_hash"], current): - flash("Current password is incorrect.", "error") - elif len(new) < 4: - flash("New password must be at least 4 characters.", "error") - elif new != confirm: - flash("New passwords do not match.", "error") - else: - conn.execute("UPDATE users SET password_hash = ? WHERE id = ?", - (generate_password_hash(new), user["id"])) - conn.commit() - flash("Password changed.", "ok") - return redirect(url_for("admin")) - - -@app.route("/account/mfa/setup", methods=["GET", "POST"]) -@login_required -def mfa_setup(): - conn = get_db() - user = conn.execute("SELECT * FROM users WHERE id = ?", (session["user_id"],)).fetchone() - if request.method == "POST": - code = request.form.get("code", "").strip().replace(" ", "") - secret = session.get("mfa_setup_secret") - if secret and pyotp.TOTP(secret).verify(code, valid_window=1): - conn.execute("UPDATE users SET mfa_secret = ?, mfa_enabled = 1 WHERE id = ?", - (secret, user["id"])) - conn.commit() - session.pop("mfa_setup_secret", None) - flash("Two-factor authentication is now enabled.", "ok") - return redirect(url_for("admin")) - flash("That code did not match. Scan the QR code and try again.", "error") - secret = session.get("mfa_setup_secret") or pyotp.random_base32() - session["mfa_setup_secret"] = secret - uri = pyotp.TOTP(secret).provisioning_uri( - name=user["username"], issuer_name="Birthday Manager") - buf = io.BytesIO() - qrcode.make(uri).save(buf, format="PNG") - qr_b64 = base64.b64encode(buf.getvalue()).decode() - return render_template("mfa_setup.html", footer=FOOTER_TEXT, - username=session.get("username"), - qr=qr_b64, secret=secret) - - -@app.route("/account/mfa/disable", methods=["POST"]) -@login_required -def mfa_disable(): - conn = get_db() - conn.execute("UPDATE users SET mfa_enabled = 0, mfa_secret = NULL WHERE id = ?", - (session["user_id"],)) - conn.commit() - flash("Two-factor authentication disabled for your account.", "ok") - return redirect(url_for("admin")) - - -@app.route("/templates/save", methods=["POST"]) -@login_required -def save_template(): - gender = request.form.get("gender") - if gender not in ("Male", "Female"): - flash("Unknown template.", "error") - return redirect(url_for("admin")) - subject = request.form.get("subject", "").strip() or "Happy Birthday, {first_name}!" - body = request.form.get("body", "").strip() - get_db().execute("UPDATE templates SET subject = ?, body = ? WHERE gender = ?", - (subject, body, gender)) - get_db().commit() - flash(f"E-mail template ({gender}) saved.", "ok") - return redirect(url_for("admin")) - - -@app.route("/images/upload", methods=["POST"]) -@login_required -def upload_image(): - file = request.files.get("image") - if not file or not file.filename: - flash("Choose an image file to upload.", "error") - return redirect(url_for("admin")) - name = secure_filename(file.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in ALLOWED_IMAGE_EXT: - flash("Only PNG, JPG, GIF or WEBP images are allowed.", "error") - return redirect(url_for("admin")) - unique = f"{os.path.splitext(name)[0]}-{uuid.uuid4().hex[:6]}{ext}" - file.save(os.path.join(UPLOAD_DIR, unique)) - flash(f"Image uploaded. Insert it in a template with: ", "ok") - return redirect(url_for("admin")) - - -@app.route("/images//delete", methods=["POST"]) -@login_required -def delete_image(fname): - path = os.path.join(UPLOAD_DIR, secure_filename(fname)) - if os.path.isfile(path): - os.remove(path) - flash("Image deleted.", "ok") - return redirect(url_for("admin")) - - -@app.route("/uploads/") -@login_required -def serve_upload(fname): - return send_from_directory(UPLOAD_DIR, secure_filename(fname)) - - -# ---------------------------------------------------------------- startup - -init_db() -scheduler = BackgroundScheduler(daemon=True) -send_hour = int(os.environ.get("SEND_HOUR", "8")) -scheduler.add_job(daily_birthday_job, "cron", hour=send_hour, minute=0, - id="daily-birthday-mail") -scheduler.start() - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8080")), debug=False) diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/docker-compose.yml b/Martinhal Birthday Mailing App/v1.1/birthday-app/docker-compose.yml deleted file mode 100644 index 10ea775..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - birthday-manager: - build: . - container_name: birthday-manager - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - ./data:/data - environment: - # --- Purelymail account (fill in your mailbox credentials) --- - SMTP_USER: "you@yourdomain.com" - SMTP_PASS: "your-purelymail-password" - SMTP_FROM: "you@yourdomain.com" - - # --- Outgoing mail (pre-configured for Purelymail, SSL/TLS) --- - SMTP_HOST: "smtp.purelymail.com" - SMTP_PORT: "465" - SMTP_SECURITY: "ssl" # if your network only allows STARTTLS: port 587 + "starttls" - - # --- Incoming mail / Sent-folder copy (pre-configured for Purelymail) --- - IMAP_HOST: "imap.purelymail.com" - IMAP_PORT: "993" # SSL/TLS - # IMAP_USER / IMAP_PASS default to the SMTP credentials above - IMAP_SENT_FOLDER: "Sent" - - # --- optional --- - SEND_HOUR: "8" # hour of day (0-23) automatic e-mails go out - SECRET_KEY: "change-this-to-a-long-random-string" - TZ: "Europe/Lisbon" diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/requirements.txt b/Martinhal Birthday Mailing App/v1.1/birthday-app/requirements.txt deleted file mode 100644 index 98270a0..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flask>=3.0 -pyotp>=2.9 -qrcode[pil]>=7.4 -APScheduler>=3.10 -werkzeug>=3.0 diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/static/style.css b/Martinhal Birthday Mailing App/v1.1/birthday-app/static/style.css deleted file mode 100644 index 500ee5e..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/static/style.css +++ /dev/null @@ -1,230 +0,0 @@ -/* Birthday Manager — © 2026 Martinhal IT */ -:root { - --pine: #123f3a; - --pine-deep: #0c2b28; - --paper: #f6f4ee; - --card: #ffffff; - --ink: #21302d; - --muted: #6b7a76; - --line: #e2ddd1; - --marigold: #e8a13a; - --marigold-deep: #c07f1c; - --danger: #b3402a; - --ok: #2e7d4f; - font-size: 16px; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - min-height: 100vh; - display: flex; - flex-direction: column; - background: var(--paper); - color: var(--ink); - font-family: "Inter", "Segoe UI", system-ui, sans-serif; -} - -h1, h2, h3, .brand-name, .cal-year { - font-family: "Sora", "Segoe UI", system-ui, sans-serif; - letter-spacing: -0.01em; -} -h1 { font-size: 1.6rem; margin: 0.2rem 0; } -h2 { font-size: 1.15rem; margin-top: 0; } -h3 { font-size: 0.9rem; margin: 0 0 0.4rem; } - -/* ---------- top bar ---------- */ -.topbar { - display: flex; - align-items: center; - gap: 2rem; - padding: 0.7rem 1.4rem; - background: var(--pine); - color: #f2efe6; -} -.brand { display: flex; align-items: center; gap: 0.55rem; font-weight: 700; } -.brand-mark { font-size: 1.3rem; } -.nav { display: flex; gap: 0.4rem; flex: 1; } -.nav a { - color: #cfe0d8; - text-decoration: none; - padding: 0.45rem 0.9rem; - border-radius: 8px; - font-weight: 500; -} -.nav a:hover { background: rgba(255,255,255,0.08); color: #fff; } -.nav a.active { background: var(--marigold); color: var(--pine-deep); font-weight: 600; } -.userbox { display: flex; align-items: center; gap: 0.7rem; } -.userchip { - background: rgba(255,255,255,0.12); - padding: 0.3rem 0.75rem; - border-radius: 999px; - font-size: 0.85rem; -} - -/* ---------- layout ---------- */ -.page { - flex: 1; - width: min(1180px, 100%); - margin: 0 auto; - padding: 1.4rem 1.2rem 2.5rem; -} -.card { - background: var(--card); - border: 1px solid var(--line); - border-radius: 14px; - padding: 1.2rem 1.4rem; - margin-bottom: 1.3rem; - box-shadow: 0 1px 2px rgba(18, 63, 58, 0.05); -} -.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 1.3rem; } -.two-col .card { margin-bottom: 1.3rem; } -@media (max-width: 860px) { .two-col { grid-template-columns: 1fr; } } - -.footer { - text-align: center; - padding: 1rem; - background: var(--pine-deep); - color: #cfe0d8; - font-size: 0.85rem; -} - -/* ---------- today banner ---------- */ -.today-banner { - display: flex; - gap: 1.5rem; - align-items: center; - justify-content: space-between; - border-left: 6px solid var(--marigold); -} -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.12em; - font-size: 0.72rem; - color: var(--muted); - margin: 0; -} -.today-people { list-style: none; margin: 0.4rem 0 0; padding: 0; } -.today-people li { margin: 0.25rem 0; } -.today-right { text-align: right; flex-shrink: 0; max-width: 220px; } - -/* ---------- forms ---------- */ -label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; font-weight: 500; } -input, select, textarea { - font: inherit; - padding: 0.5rem 0.65rem; - border: 1px solid var(--line); - border-radius: 8px; - background: #fdfcf9; -} -input:focus, select:focus, textarea:focus, button:focus-visible, a:focus-visible { - outline: 2px solid var(--marigold); - outline-offset: 1px; -} -textarea { resize: vertical; font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 0.85rem; } -.entry-form { - display: flex; - flex-wrap: wrap; - gap: 0.8rem; - align-items: flex-end; - margin-bottom: 1rem; -} -.entry-form label { flex: 1 1 150px; } -.stack-form { display: flex; flex-direction: column; gap: 0.8rem; align-items: flex-start; } -.stack-form label { width: 100%; } -.row-gap { display: flex; gap: 0.7rem; margin-top: 0.8rem; } - -/* ---------- buttons ---------- */ -.btn { - font: inherit; - font-weight: 600; - border: none; - border-radius: 9px; - padding: 0.55rem 1.1rem; - cursor: pointer; - text-decoration: none; - display: inline-block; -} -.btn-primary { background: var(--pine); color: #fff; } -.btn-primary:hover { background: var(--pine-deep); } -.btn-accent { background: var(--marigold); color: var(--pine-deep); } -.btn-accent:hover { background: var(--marigold-deep); color: #fff; } -.btn-danger { background: #fbeae5; color: var(--danger); } -.btn-danger:hover { background: var(--danger); color: #fff; } -.btn-ghost { background: transparent; color: inherit; border: 1px solid currentColor; opacity: 0.85; } -.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.8rem; } -.btn-lg { padding: 0.8rem 1.6rem; font-size: 1.05rem; } -.btn-block { width: 100%; } -.btn[disabled] { opacity: 0.45; cursor: not-allowed; } - -/* ---------- tables ---------- */ -.table-wrap { overflow-x: auto; } -table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } -th { text-align: left; color: var(--muted); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; } -th, td { padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--line); } -tbody tr:hover { background: #faf8f2; } -.ta-right { text-align: right; } -.nowrap { white-space: nowrap; } -.log-wrap { max-height: 420px; overflow-y: auto; } - -/* ---------- pills / flash ---------- */ -.pill { border-radius: 999px; padding: 0.15rem 0.6rem; font-size: 0.72rem; font-weight: 600; } -.pill-ok { background: #e2f2e8; color: var(--ok); } -.pill-bad { background: #fbeae5; color: var(--danger); } -.pill-wait { background: #fdf1dc; color: var(--marigold-deep); } -.flash { padding: 0.7rem 1rem; border-radius: 10px; margin-bottom: 1rem; font-size: 0.9rem; } -.flash-ok { background: #e2f2e8; color: var(--ok); } -.flash-error { background: #fbeae5; color: var(--danger); } - -/* ---------- yearly calendar ---------- */ -.cal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; } -.cal-nav { display: flex; align-items: center; gap: 0.7rem; } -.cal-year { font-size: 1.2rem; font-weight: 700; } -.year-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); - gap: 1rem; -} -.mini-cal { font-size: 0.72rem; } -.mini-cal th, .mini-cal td { padding: 0.22rem; text-align: center; border: none; } -.mini-cal td { position: relative; border-radius: 6px; } -.mini-cal td.bday { background: #fdf1dc; font-weight: 700; color: var(--marigold-deep); cursor: help; } -.mini-cal td.today { outline: 2px solid var(--pine); } -.mini-cal .dot { - position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); - width: 4px; height: 4px; border-radius: 50%; background: var(--marigold-deep); -} - -/* ---------- images ---------- */ -.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-top: 1rem; } -.image-tile { - border: 1px solid var(--line); border-radius: 10px; padding: 0.6rem; - display: flex; flex-direction: column; gap: 0.5rem; align-items: center; text-align: center; -} -.image-tile img { max-width: 100%; max-height: 110px; object-fit: contain; border-radius: 6px; } -.image-tile code { word-break: break-all; } - -/* ---------- auth / misc ---------- */ -.auth-card { - background: var(--card); border: 1px solid var(--line); border-radius: 14px; - max-width: 380px; margin: 8vh auto 0; padding: 2rem; - display: flex; flex-direction: column; gap: 0.4rem; -} -.auth-card.wide { max-width: 480px; margin-top: 3vh; } -.auth-card form { display: flex; flex-direction: column; gap: 0.9rem; margin-top: 0.8rem; } -.qr-wrap { text-align: center; margin: 0.5rem 0; } -.qr-wrap img { width: 190px; height: 190px; } -.muted { color: var(--muted); } -.small { font-size: 0.8rem; } -code { background: #f0ede4; padding: 0.1rem 0.35rem; border-radius: 5px; font-size: 0.82em; } -.tpl-zone textarea { width: 100%; } - -@media (prefers-reduced-motion: no-preference) { - .btn { transition: background 0.15s ease, color 0.15s ease; } -} -@media (max-width: 700px) { - .topbar { flex-wrap: wrap; gap: 0.6rem; } - .today-banner { flex-direction: column; align-items: flex-start; } - .today-right { text-align: left; } -} diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/admin.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/admin.html deleted file mode 100644 index e3c7955..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/admin.html +++ /dev/null @@ -1,148 +0,0 @@ -{% extends "base.html" %} -{% set active = 'admin' %} -{% block title %}Administration - Birthday Manager{% endblock %} -{% block content %} - - -
    -

    Users

    -
    - - - -
    -
    - - - - {% for u in users %} - - - - - - - {% endfor %} - -
    UsernameMFACreated
    {{ u.username }}{% if u.id == user_id %} you{% endif %}{{ 'Enabled' if u.mfa_enabled else 'Off' }}{{ u.created_at }} - {% if u.id != user_id %} -
    - -
    - {% endif %} -
    -
    -
    - - -
    -
    -

    Change my password

    -
    - - - - -
    -
    - -
    -

    Two-factor authentication (MFA)

    - {% set me = users | selectattr('id', 'equalto', user_id) | first %} - {% if me and me.mfa_enabled %} -

    MFA is enabled for your account. You'll be asked for a 6-digit code every time you sign in.

    -
    - -
    - {% else %} -

    Protect your account with a one-time code from an authenticator app (Google Authenticator, Authy, Microsoft Authenticator…).

    - Enable MFA - {% endif %} -
    -
    - - -
    -

    E-mail log

    - {% if log %} -
    - - - - {% for l in log %} - - - - - - - - {% endfor %} - -
    Date & timeRecipientE-mail addressSubjectStatus
    {{ l.sent_at }}{{ l.recipient_name }}{{ l.recipient_email }}{{ l.subject }} - {{ l.status }} -
    -
    - {% else %} -

    No e-mails have been sent yet.

    - {% endif %} -
    - - -
    -

    Images

    -

    Upload images here, then place them inside a template with - <img src="/uploads/filename">. Images are embedded inline in the e-mail when it is sent.

    -
    - - -
    - {% if images %} -
    - {% for img in images %} -
    - {{ img }} - /uploads/{{ img }} -
    - -
    -
    - {% endfor %} -
    - {% endif %} -
    - - -
    -
    -

    E-mail Template Male

    -
    - - - -

    Placeholders: {first_name} {last_name} {age} — insert images with <img src="/uploads/…">

    - -
    -
    - -
    -

    E-Mail Template Female

    -
    - - - -

    Placeholders: {first_name} {last_name} {age} — insert images with <img src="/uploads/…">

    - -
    -
    -
    - -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/base.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/base.html deleted file mode 100644 index 522cb7c..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/base.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - {% block title %}Birthday Manager{% endblock %} - - - - - -
    -
    - 🎂 - Birthday Manager -
    - {% if username %} - -
    - {{ username }} - Log out -
    - {% endif %} -
    - -
    - {% with messages = get_flashed_messages(with_categories=true) %} - {% for category, msg in messages %} -
    {{ msg }}
    - {% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
    - -
    {{ footer }}
    - - diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/dashboard.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/dashboard.html deleted file mode 100644 index 5742ced..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/dashboard.html +++ /dev/null @@ -1,118 +0,0 @@ -{% extends "base.html" %} -{% set active = 'dashboard' %} -{% block title %}Dashboard - Birthday Manager{% endblock %} -{% block content %} - - -
    -
    -

    Today, {{ today.strftime('%A %d %B %Y') }}

    - {% if today_list %} -

    🎉 Birthday{{ 's' if today_list|length > 1 }} today

    -
      - {% for item in today_list %} -
    • - {{ item.row.first_name }} {{ item.row.last_name }} - turns {{ item.age }} - - {{ 'e-mail sent' if item.sent else 'e-mail pending' }} - -
    • - {% endfor %} -
    - {% else %} -

    No birthdays today

    -

    The next celebration is waiting on the calendar below.

    - {% endif %} -
    -
    - -

    Sends today's birthday e-mail{{ 's' if today_list|length != 1 }} again immediately.

    -
    -
    - - -
    -

    Birthday entries

    -
    - - - - - - -
    - - {% if entries %} -
    - - - - - - {% for e in entries %} - - - - - - - - - {% endfor %} - -
    First NameLast NameDate of BirthGenderE-mail
    {{ e.first_name }}{{ e.last_name }}{{ e.dob }}{{ e.gender }}{{ e.email }} -
    - -
    -
    -
    - {% else %} -

    No entries yet. Add the first birthday above.

    - {% endif %} -
    - - -
    -
    -

    Yearly overview

    - -
    -
    - {% for month in months %} -
    -

    {{ month.name }}

    - - - - {% for week in month.weeks %} - - {% for cell in week %} - - {% endfor %} - - {% endfor %} - -
    MoTuWeThFrSaSu
    - {{ cell.day }} - {% if cell.names %}{% endif %} -
    -
    - {% endfor %} -
    -

    Days marked with a dot have a birthday — hover to see who. Today is outlined.

    -
    - -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/login.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/login.html deleted file mode 100644 index 9a8afa3..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/login.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in - Birthday Manager{% endblock %} -{% block content %} -
    -

    Sign in

    -

    Birthday Manager

    -
    - - - -
    -
    -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_setup.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_setup.html deleted file mode 100644 index 9c3e3f0..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_setup.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "base.html" %} -{% set active = 'admin' %} -{% block title %}Enable two-factor authentication{% endblock %} -{% block content %} -
    -

    Enable two-factor authentication

    -

    Scan this QR code with Google Authenticator, Microsoft Authenticator, Authy or any TOTP app, then enter the 6-digit code it shows to confirm.

    -
    - MFA QR code -

    Can't scan? Enter this key manually: {{ secret }}

    -
    -
    - -
    - - Cancel -
    -
    -
    -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_verify.html b/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_verify.html deleted file mode 100644 index d0cdae8..0000000 --- a/Martinhal Birthday Mailing App/v1.1/birthday-app/templates/mfa_verify.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} -{% block title %}Two-factor authentication{% endblock %} -{% block content %} -
    -

    Two-factor code

    -

    Enter the 6-digit code from your authenticator app.

    -
    - - -
    -
    -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app.zip b/Martinhal Birthday Mailing App/v1.2/birthday-app.zip deleted file mode 100644 index b5646ce..0000000 Binary files a/Martinhal Birthday Mailing App/v1.2/birthday-app.zip and /dev/null differ diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/Dockerfile b/Martinhal Birthday Mailing App/v1.2/birthday-app/Dockerfile deleted file mode 100644 index 390c06a..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.12-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -ENV DATA_DIR=/data PORT=8080 -VOLUME /data -EXPOSE 8080 -CMD ["python", "app.py"] diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/README.md b/Martinhal Birthday Mailing App/v1.2/birthday-app/README.md deleted file mode 100644 index 631eccf..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# Birthday Manager - -Self-hosted birthday reminder application that automatically e-mails people on their birthday. - -© 2026 Martinhal IT - Joao Vaz - Version 1.2 - -## Features - -**Page 1 — Dashboard** -- Three panels at the top: **Yesterday's Birthdays**, **Today's Birthdays** (with the **Force Send** button to resend the e-mail) and **Tomorrow's Birthdays** -- Add / remove birthday entries: First Name, Last Name, Date of Birth, Gender, E-mail address -- Yearly calendar overview of all entries (hover a marked day to see the names; browse other years) - -**Page 2 — Administration** -- Add / remove application users -- Every user can enable/disable MFA (TOTP — Google Authenticator, Authy, etc.) and change their own password -- Full e-mail log: who received mail, at what date & time, and whether it succeeded -- Setting for the time of day the automatic birthday e-mails are sent (applies immediately, no restart needed) -- Two template zones: **E-mail Template Male** and **E-Mail Template Female**, with placeholders `{first_name}`, `{last_name}`, `{age}` -- Image upload — select multiple files at once (15 MB total per upload); each image shows its ready-to-copy `` command, and images are embedded inline in the outgoing e-mail - -Automatic sending: a scheduler runs every day at `SEND_HOUR` (default 08:00) and e-mails everyone whose birthday it is, using the template matching their gender. Duplicates are avoided (one automatic mail per person per day); **Force Send** always resends. February 29 birthdays are celebrated on February 28 in non-leap years. - -Every successfully sent e-mail is also copied to the mailbox's **Sent** folder via IMAP, so it shows up in your normal mail client. - -## Mail provider - -The app comes pre-configured for **Purelymail**: - -| | Server | Port | Security | -|---|---|---|---| -| Outgoing (SMTP) | `smtp.purelymail.com` | `465` | SSL/TLS | -| Incoming (IMAP, Sent-folder copy) | `imap.purelymail.com` | `993` | SSL/TLS | - -You only need to fill in `SMTP_USER`, `SMTP_PASS` and `SMTP_FROM` with your Purelymail mailbox credentials. If your network only supports STARTTLS for SMTP, set `SMTP_PORT=587` and `SMTP_SECURITY=starttls` instead. Any other provider works too — just override the host/port variables. - -## Quick start (Docker — recommended) - -1. Edit `docker-compose.yml` and fill in your Purelymail credentials (`SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`) plus a random `SECRET_KEY` and your timezone. -2. Run: - -```bash -docker compose up -d --build -``` - -3. Open http://localhost:8080 and log in with **admin / admin** — then change the password on the Administration page immediately. - -All data (SQLite database + uploaded images) lives in `./data`, so back up that folder. - -## Quick start (plain Python) - -```bash -pip install -r requirements.txt - -# Purelymail servers are the built-in defaults - only credentials are needed -export SMTP_USER=you@yourdomain.com -export SMTP_PASS=your-purelymail-password -export SMTP_FROM=you@yourdomain.com -export SEND_HOUR=8 -export SECRET_KEY=$(python -c "import secrets;print(secrets.token_hex(32))") - -python app.py -``` - -The app listens on port 8080 (`PORT` env var to change). Data is stored in `./data` (`DATA_DIR` env var to change). - -## Environment variables - -| Variable | Default | Purpose | -|---|---|---| -| `SMTP_HOST` | `smtp.purelymail.com` | SMTP server | -| `SMTP_PORT` | `465` | SMTP port | -| `SMTP_USER` | – | SMTP username / mailbox | -| `SMTP_PASS` | – | SMTP password | -| `SMTP_FROM` | `SMTP_USER` | From address | -| `SMTP_SECURITY` | `ssl` | `ssl`, `starttls` or `none` | -| `IMAP_HOST` | `imap.purelymail.com` | IMAP server for the Sent-folder copy | -| `IMAP_PORT` | `993` | IMAP port (SSL/TLS) | -| `IMAP_USER` | `SMTP_USER` | IMAP username | -| `IMAP_PASS` | `SMTP_PASS` | IMAP password | -| `IMAP_SENT_FOLDER` | `Sent` | Mailbox folder sent mail is stored in | -| `SEND_HOUR` | `8` | Default hour for the daily send — the time picked on the Administration page overrides this | -| `SECRET_KEY` | random | Flask session key — set it so logins survive restarts | -| `PORT` | `8080` | HTTP port | -| `DATA_DIR` | `./data` | Where the database and uploads are stored | - -## Notes - -- The e-mail address field on entries is required because it is where the birthday greeting is sent. -- Failed sends (e.g. SMTP misconfigured) are also recorded in the e-mail log with the error as tooltip on the "Failed" status. If a mail is delivered but the IMAP copy to the Sent folder fails, the mail is still logged as Sent and the IMAP error appears as a tooltip. -- Run behind a reverse proxy (nginx / Caddy / Traefik) with HTTPS if you expose it beyond your LAN. diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/app.py b/Martinhal Birthday Mailing App/v1.2/birthday-app/app.py deleted file mode 100644 index 314d7c8..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/app.py +++ /dev/null @@ -1,724 +0,0 @@ -""" -Birthday Manager - self-hosted birthday reminder & e-mail application -(c) 2026 Martinhal IT - Joao Vaz - Version 1.2 -""" -import base64 -import calendar as cal -import imaplib -import io -import os -import re -import secrets -import smtplib -import sqlite3 -import uuid -from datetime import date, datetime -from email.mime.image import MIMEImage -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from functools import wraps - -import pyotp -import qrcode -from apscheduler.schedulers.background import BackgroundScheduler -from flask import (Flask, flash, g, redirect, render_template, request, - send_from_directory, session, url_for) -from werkzeug.security import check_password_hash, generate_password_hash -from werkzeug.utils import secure_filename - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -DATA_DIR = os.environ.get("DATA_DIR", os.path.join(BASE_DIR, "data")) -UPLOAD_DIR = os.path.join(DATA_DIR, "uploads") -DB_PATH = os.path.join(DATA_DIR, "birthday.db") -ALLOWED_IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp"} - -os.makedirs(UPLOAD_DIR, exist_ok=True) - -app = Flask(__name__) -app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32)) -app.config["MAX_CONTENT_LENGTH"] = 15 * 1024 * 1024 # 15 MB total upload limit - -APP_VERSION = "Version 1.2" -FOOTER_TEXT = "\u00a9 2026 Martinhal IT - Joao Vaz - " + APP_VERSION - -# ---------------------------------------------------------------- database - -def get_db(): - if "db" not in g: - g.db = sqlite3.connect(DB_PATH) - g.db.row_factory = sqlite3.Row - g.db.execute("PRAGMA foreign_keys = ON") - return g.db - - -@app.teardown_appcontext -def close_db(_exc): - db = g.pop("db", None) - if db is not None: - db.close() - - -def db_connect(): - """Standalone connection for use outside a request (scheduler).""" - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn - - -DEFAULT_TEMPLATE_MALE = ( - "

    Dear {first_name} {last_name},

    " - "

    Happy Birthday! Wishing you a fantastic day and a wonderful year ahead. " - "Congratulations on turning {age}!

    " - "

    Best regards,
    Martinhal IT

    " -) -DEFAULT_TEMPLATE_FEMALE = ( - "

    Dear {first_name} {last_name},

    " - "

    Happy Birthday! Wishing you a beautiful day filled with joy. " - "Congratulations on turning {age}!

    " - "

    Warm regards,
    Martinhal IT

    " -) - - -def init_db(): - conn = db_connect() - conn.executescript( - """ - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - mfa_secret TEXT, - mfa_enabled INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS birthdays ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - dob TEXT NOT NULL, -- YYYY-MM-DD - gender TEXT NOT NULL, -- Male / Female - email TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS email_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - recipient_name TEXT NOT NULL, - recipient_email TEXT NOT NULL, - subject TEXT NOT NULL, - sent_at TEXT NOT NULL, -- YYYY-MM-DD HH:MM:SS - status TEXT NOT NULL, -- Sent / Failed - detail TEXT - ); - CREATE TABLE IF NOT EXISTS templates ( - gender TEXT PRIMARY KEY, -- Male / Female - subject TEXT NOT NULL, - body TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - """ - ) - # Seed default admin user - if conn.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] == 0: - conn.execute( - "INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)", - ("admin", generate_password_hash("admin"), - datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ) - print(" * Created default user 'admin' with password 'admin' - change it!") - # Seed default templates - for gender, body in (("Male", DEFAULT_TEMPLATE_MALE), - ("Female", DEFAULT_TEMPLATE_FEMALE)): - conn.execute( - "INSERT OR IGNORE INTO templates (gender, subject, body) VALUES (?,?,?)", - (gender, "Happy Birthday, {first_name}!", body), - ) - conn.commit() - conn.close() - - -def get_setting(conn, key, default=None): - row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone() - return row["value"] if row else default - - -def set_setting(conn, key, value): - conn.execute( - "INSERT INTO settings (key, value) VALUES (?,?)" - " ON CONFLICT(key) DO UPDATE SET value = excluded.value", (key, value)) - conn.commit() - - -def get_send_time(conn) -> tuple[int, int]: - default = f'{int(os.environ.get("SEND_HOUR", "8")):02d}:00' - raw = get_setting(conn, "send_time", default) - try: - h, m = raw.split(":") - return max(0, min(23, int(h))), max(0, min(59, int(m))) - except ValueError: - return 8, 0 - - -# ---------------------------------------------------------------- auth - -def login_required(view): - @wraps(view) - def wrapped(*args, **kwargs): - if not session.get("user_id"): - return redirect(url_for("login", next=request.path)) - return view(*args, **kwargs) - return wrapped - - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if request.method == "POST": - username = request.form.get("username", "").strip() - password = request.form.get("password", "") - user = get_db().execute( - "SELECT * FROM users WHERE username = ?", (username,) - ).fetchone() - if user and check_password_hash(user["password_hash"], password): - if user["mfa_enabled"]: - session.clear() - session["mfa_pending_user"] = user["id"] - return redirect(url_for("mfa_verify")) - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - return redirect(url_for("dashboard")) - flash("Invalid username or password.", "error") - return render_template("login.html", footer=FOOTER_TEXT) - - -@app.route("/mfa-verify", methods=["GET", "POST"]) -def mfa_verify(): - pending = session.get("mfa_pending_user") - if not pending: - return redirect(url_for("login")) - if request.method == "POST": - code = request.form.get("code", "").strip().replace(" ", "") - user = get_db().execute("SELECT * FROM users WHERE id = ?", (pending,)).fetchone() - if user and user["mfa_secret"] and pyotp.TOTP(user["mfa_secret"]).verify(code, valid_window=1): - session.clear() - session["user_id"] = user["id"] - session["username"] = user["username"] - return redirect(url_for("dashboard")) - flash("Invalid authentication code.", "error") - return render_template("mfa_verify.html", footer=FOOTER_TEXT) - - -@app.route("/logout") -def logout(): - session.clear() - return redirect(url_for("login")) - - -# ---------------------------------------------------------------- helpers - -def calc_age(dob: date, on: date) -> int: - years = on.year - dob.year - if (on.month, on.day) < (dob.month, dob.day): - years -= 1 - return years - - -def birthday_matches_day(dob: date, day: date) -> bool: - """Feb 29 birthdays are celebrated on Feb 28 in non-leap years.""" - if dob.month == 2 and dob.day == 29 and not cal.isleap(day.year): - return day.month == 2 and day.day == 28 - return dob.month == day.month and dob.day == day.day - - -def birthdays_on(conn, day: date): - rows = conn.execute("SELECT * FROM birthdays").fetchall() - result = [] - for r in rows: - dob = datetime.strptime(r["dob"], "%Y-%m-%d").date() - if birthday_matches_day(dob, day): - result.append((r, calc_age(dob, day))) - return result - - -def todays_birthdays(conn): - return birthdays_on(conn, date.today()) - - -def render_placeholders(text: str, row, age: int) -> str: - return (text.replace("{first_name}", row["first_name"]) - .replace("{last_name}", row["last_name"]) - .replace("{age}", str(age))) - - -def smtp_settings(): - # Defaults are pre-configured for Purelymail (smtp.purelymail.com:465 SSL/TLS). - # If your client only supports STARTTLS, set SMTP_PORT=587 and SMTP_SECURITY=starttls. - return { - "host": os.environ.get("SMTP_HOST", "smtp.purelymail.com"), - "port": int(os.environ.get("SMTP_PORT", "465")), - "user": os.environ.get("SMTP_USER", ""), - "password": os.environ.get("SMTP_PASS", ""), - "sender": os.environ.get("SMTP_FROM", os.environ.get("SMTP_USER", "birthday@localhost")), - "security": os.environ.get("SMTP_SECURITY", "ssl").lower(), # ssl / starttls / none - } - - -def imap_settings(): - # Defaults are pre-configured for Purelymail (imap.purelymail.com:993 SSL/TLS). - # IMAP credentials fall back to the SMTP ones if not set separately. - return { - "host": os.environ.get("IMAP_HOST", "imap.purelymail.com"), - "port": int(os.environ.get("IMAP_PORT", "993")), - "user": os.environ.get("IMAP_USER", os.environ.get("SMTP_USER", "")), - "password": os.environ.get("IMAP_PASS", os.environ.get("SMTP_PASS", "")), - "folder": os.environ.get("IMAP_SENT_FOLDER", "Sent"), - } - - -def save_to_sent_folder(msg) -> str: - """Append the sent message to the mailbox's Sent folder via IMAP. - Returns '' on success or an error description on failure.""" - cfg = imap_settings() - if not cfg["user"]: - return "IMAP not configured - message not copied to Sent folder" - try: - imap = imaplib.IMAP4_SSL(cfg["host"], cfg["port"], timeout=20) - imap.login(cfg["user"], cfg["password"]) - folder = cfg["folder"] - # Create the folder if the server doesn't have it yet - status, _ = imap.select(folder) - if status != "OK": - imap.create(folder) - imap.append(folder, r"(\Seen)", imaplib.Time2Internaldate(datetime.now().timestamp()), - msg.as_bytes()) - imap.logout() - return "" - except Exception as exc: - return f"Delivered, but copy to Sent folder failed: {exc}" - - -IMG_SRC_RE = re.compile(r'src="/uploads/([^"]+)"') - - -def build_email(row, age: int, conn) -> MIMEMultipart: - tpl = conn.execute( - "SELECT * FROM templates WHERE gender = ?", (row["gender"],) - ).fetchone() - subject = render_placeholders(tpl["subject"], row, age) - body_html = render_placeholders(tpl["body"], row, age) - - msg = MIMEMultipart("related") - msg["Subject"] = subject - msg["From"] = smtp_settings()["sender"] - msg["To"] = row["email"] - - # Embed uploaded images referenced in the template as inline attachments - inline = [] - def to_cid(match): - fname = match.group(1) - path = os.path.join(UPLOAD_DIR, secure_filename(fname)) - if os.path.isfile(path): - cid = uuid.uuid4().hex - inline.append((path, cid)) - return f'src="cid:{cid}"' - return match.group(0) - - body_html = IMG_SRC_RE.sub(to_cid, body_html) - - alt = MIMEMultipart("alternative") - alt.attach(MIMEText(re.sub(r"<[^>]+>", " ", body_html), "plain", "utf-8")) - alt.attach(MIMEText(body_html, "html", "utf-8")) - msg.attach(alt) - - for path, cid in inline: - with open(path, "rb") as f: - img = MIMEImage(f.read()) - img.add_header("Content-ID", f"<{cid}>") - img.add_header("Content-Disposition", "inline", filename=os.path.basename(path)) - msg.attach(img) - return msg - - -def send_birthday_email(row, age: int, conn) -> None: - cfg = smtp_settings() - msg = build_email(row, age, conn) - status, detail = "Sent", "" - try: - if not cfg["host"]: - raise RuntimeError("SMTP is not configured (set SMTP_HOST etc.)") - if cfg["security"] == "ssl": - server = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=20) - else: - server = smtplib.SMTP(cfg["host"], cfg["port"], timeout=20) - if cfg["security"] == "starttls": - server.starttls() - if cfg["user"]: - server.login(cfg["user"], cfg["password"]) - server.send_message(msg) - server.quit() - detail = save_to_sent_folder(msg) # keep a copy in the mailbox Sent folder - except Exception as exc: # log failures too - status, detail = "Failed", str(exc) - conn.execute( - "INSERT INTO email_log (recipient_name, recipient_email, subject, sent_at, status, detail)" - " VALUES (?,?,?,?,?,?)", - (f'{row["first_name"]} {row["last_name"]}', row["email"], msg["Subject"], - datetime.now().strftime("%Y-%m-%d %H:%M:%S"), status, detail), - ) - conn.commit() - - -def already_sent_today(conn, row) -> bool: - today = date.today().strftime("%Y-%m-%d") - hit = conn.execute( - "SELECT 1 FROM email_log WHERE recipient_email = ? AND status = 'Sent'" - " AND sent_at LIKE ?", (row["email"], today + "%"), - ).fetchone() - return hit is not None - - -def daily_birthday_job(): - conn = db_connect() - try: - for row, age in todays_birthdays(conn): - if not already_sent_today(conn, row): - send_birthday_email(row, age, conn) - finally: - conn.close() - - -# ---------------------------------------------------------------- page 1 - -MONTH_NAMES = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"] - - -def build_year_calendar(conn, year: int): - """Return list of month dicts with week grids; birthday days carry names.""" - rows = conn.execute("SELECT * FROM birthdays").fetchall() - marks = {} - for r in rows: - dob = datetime.strptime(r["dob"], "%Y-%m-%d").date() - m, d = dob.month, dob.day - if m == 2 and d == 29 and not cal.isleap(year): - d = 28 - marks.setdefault((m, d), []).append(f'{r["first_name"]} {r["last_name"]}') - months = [] - c = cal.Calendar(firstweekday=0) # Monday - today = date.today() - for m in range(1, 13): - weeks = [] - for week in c.monthdayscalendar(year, m): - weeks.append([ - { - "day": d or "", - "names": marks.get((m, d), []) if d else [], - "today": bool(d) and today.year == year and today.month == m and today.day == d, - } for d in week - ]) - months.append({"name": MONTH_NAMES[m - 1], "weeks": weeks}) - return months - - -@app.route("/", methods=["GET"]) -@login_required -def dashboard(): - conn = get_db() - year = request.args.get("year", type=int) or date.today().year - entries = conn.execute( - "SELECT * FROM birthdays ORDER BY substr(dob,6) ASC" - ).fetchall() - from datetime import timedelta - today = date.today() - today_list = [ - {"row": r, "age": a, "sent": already_sent_today(conn, r)} - for r, a in todays_birthdays(conn) - ] - yesterday_list = [{"row": r, "age": a} for r, a in birthdays_on(conn, today - timedelta(days=1))] - tomorrow_list = [{"row": r, "age": a} for r, a in birthdays_on(conn, today + timedelta(days=1))] - return render_template( - "dashboard.html", - footer=FOOTER_TEXT, - username=session.get("username"), - entries=entries, - today_list=today_list, - yesterday_list=yesterday_list, - tomorrow_list=tomorrow_list, - today=today, - months=build_year_calendar(conn, year), - year=year, - ) - - -@app.route("/birthdays/add", methods=["POST"]) -@login_required -def add_birthday(): - f = request.form - first = f.get("first_name", "").strip() - last = f.get("last_name", "").strip() - dob = f.get("dob", "").strip() - gender = f.get("gender", "").strip() - email = f.get("email", "").strip() - try: - datetime.strptime(dob, "%Y-%m-%d") - assert first and last and email and gender in ("Male", "Female") - except (ValueError, AssertionError): - flash("Please fill in all fields with valid values.", "error") - return redirect(url_for("dashboard")) - get_db().execute( - "INSERT INTO birthdays (first_name, last_name, dob, gender, email) VALUES (?,?,?,?,?)", - (first, last, dob, gender, email), - ) - get_db().commit() - flash(f"Added {first} {last}.", "ok") - return redirect(url_for("dashboard")) - - -@app.route("/birthdays//delete", methods=["POST"]) -@login_required -def delete_birthday(bid): - get_db().execute("DELETE FROM birthdays WHERE id = ?", (bid,)) - get_db().commit() - flash("Entry removed.", "ok") - return redirect(url_for("dashboard")) - - -@app.route("/force-send", methods=["POST"]) -@login_required -def force_send(): - conn = get_db() - todays = todays_birthdays(conn) - if not todays: - flash("Nobody has a birthday today - nothing to send.", "error") - else: - for row, age in todays: - send_birthday_email(row, age, conn) - flash(f"Birthday e-mail resent to {len(todays)} recipient(s). Check the log on the Administration page.", "ok") - return redirect(url_for("dashboard")) - - -# ---------------------------------------------------------------- page 2 - -@app.route("/admin") -@login_required -def admin(): - conn = get_db() - users = conn.execute("SELECT id, username, mfa_enabled, created_at FROM users ORDER BY username").fetchall() - log = conn.execute("SELECT * FROM email_log ORDER BY sent_at DESC, id DESC LIMIT 500").fetchall() - tpl_male = conn.execute("SELECT * FROM templates WHERE gender='Male'").fetchone() - tpl_female = conn.execute("SELECT * FROM templates WHERE gender='Female'").fetchone() - images = sorted(os.listdir(UPLOAD_DIR)) - h, m = get_send_time(conn) - return render_template( - "admin.html", - send_time=f"{h:02d}:{m:02d}", - footer=FOOTER_TEXT, - username=session.get("username"), - user_id=session.get("user_id"), - users=users, - log=log, - tpl_male=tpl_male, - tpl_female=tpl_female, - images=images, - ) - - -@app.route("/users/add", methods=["POST"]) -@login_required -def add_user(): - username = request.form.get("username", "").strip() - password = request.form.get("password", "") - if not username or len(password) < 4: - flash("Username and a password of at least 4 characters are required.", "error") - return redirect(url_for("admin")) - try: - get_db().execute( - "INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)", - (username, generate_password_hash(password), - datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ) - get_db().commit() - flash(f"User '{username}' created.", "ok") - except sqlite3.IntegrityError: - flash("That username already exists.", "error") - return redirect(url_for("admin")) - - -@app.route("/users//delete", methods=["POST"]) -@login_required -def delete_user(uid): - conn = get_db() - if uid == session.get("user_id"): - flash("You cannot delete the account you are logged in with.", "error") - elif conn.execute("SELECT COUNT(*) c FROM users").fetchone()["c"] <= 1: - flash("At least one user must remain.", "error") - else: - conn.execute("DELETE FROM users WHERE id = ?", (uid,)) - conn.commit() - flash("User removed.", "ok") - return redirect(url_for("admin")) - - -@app.route("/account/password", methods=["POST"]) -@login_required -def change_password(): - current = request.form.get("current", "") - new = request.form.get("new", "") - confirm = request.form.get("confirm", "") - conn = get_db() - user = conn.execute("SELECT * FROM users WHERE id = ?", (session["user_id"],)).fetchone() - if not check_password_hash(user["password_hash"], current): - flash("Current password is incorrect.", "error") - elif len(new) < 4: - flash("New password must be at least 4 characters.", "error") - elif new != confirm: - flash("New passwords do not match.", "error") - else: - conn.execute("UPDATE users SET password_hash = ? WHERE id = ?", - (generate_password_hash(new), user["id"])) - conn.commit() - flash("Password changed.", "ok") - return redirect(url_for("admin")) - - -@app.route("/account/mfa/setup", methods=["GET", "POST"]) -@login_required -def mfa_setup(): - conn = get_db() - user = conn.execute("SELECT * FROM users WHERE id = ?", (session["user_id"],)).fetchone() - if request.method == "POST": - code = request.form.get("code", "").strip().replace(" ", "") - secret = session.get("mfa_setup_secret") - if secret and pyotp.TOTP(secret).verify(code, valid_window=1): - conn.execute("UPDATE users SET mfa_secret = ?, mfa_enabled = 1 WHERE id = ?", - (secret, user["id"])) - conn.commit() - session.pop("mfa_setup_secret", None) - flash("Two-factor authentication is now enabled.", "ok") - return redirect(url_for("admin")) - flash("That code did not match. Scan the QR code and try again.", "error") - secret = session.get("mfa_setup_secret") or pyotp.random_base32() - session["mfa_setup_secret"] = secret - uri = pyotp.TOTP(secret).provisioning_uri( - name=user["username"], issuer_name="Birthday Manager") - buf = io.BytesIO() - qrcode.make(uri).save(buf, format="PNG") - qr_b64 = base64.b64encode(buf.getvalue()).decode() - return render_template("mfa_setup.html", footer=FOOTER_TEXT, - username=session.get("username"), - qr=qr_b64, secret=secret) - - -@app.route("/account/mfa/disable", methods=["POST"]) -@login_required -def mfa_disable(): - conn = get_db() - conn.execute("UPDATE users SET mfa_enabled = 0, mfa_secret = NULL WHERE id = ?", - (session["user_id"],)) - conn.commit() - flash("Two-factor authentication disabled for your account.", "ok") - return redirect(url_for("admin")) - - -@app.route("/settings/send-time", methods=["POST"]) -@login_required -def save_send_time(): - raw = request.form.get("send_time", "").strip() - try: - h, m = raw.split(":") - h, m = int(h), int(m) - assert 0 <= h <= 23 and 0 <= m <= 59 - except (ValueError, AssertionError): - flash("Please pick a valid time.", "error") - return redirect(url_for("admin")) - set_setting(get_db(), "send_time", f"{h:02d}:{m:02d}") - scheduler.reschedule_job("daily-birthday-mail", trigger="cron", hour=h, minute=m) - flash(f"Automatic birthday e-mails will now be sent daily at {h:02d}:{m:02d}.", "ok") - return redirect(url_for("admin")) - - -@app.route("/templates/save", methods=["POST"]) -@login_required -def save_template(): - gender = request.form.get("gender") - if gender not in ("Male", "Female"): - flash("Unknown template.", "error") - return redirect(url_for("admin")) - subject = request.form.get("subject", "").strip() or "Happy Birthday, {first_name}!" - body = request.form.get("body", "").strip() - get_db().execute("UPDATE templates SET subject = ?, body = ? WHERE gender = ?", - (subject, body, gender)) - get_db().commit() - flash(f"E-mail template ({gender}) saved.", "ok") - return redirect(url_for("admin")) - - -@app.route("/images/upload", methods=["POST"]) -@login_required -def upload_image(): - files = [f for f in request.files.getlist("images") if f and f.filename] - if not files: - flash("Choose one or more image files to upload.", "error") - return redirect(url_for("admin")) - # Enforce the 15 MB total limit across all files in this upload - total = 0 - for f in files: - f.stream.seek(0, os.SEEK_END) - total += f.stream.tell() - f.stream.seek(0) - if total > 15 * 1024 * 1024: - flash(f"Upload rejected: total size {total / (1024*1024):.1f} MB exceeds the 15 MB limit.", "error") - return redirect(url_for("admin")) - saved, skipped = [], [] - for f in files: - name = secure_filename(f.filename) - ext = os.path.splitext(name)[1].lower() - if ext not in ALLOWED_IMAGE_EXT: - skipped.append(f.filename) - continue - unique = f"{os.path.splitext(name)[0]}-{uuid.uuid4().hex[:6]}{ext}" - f.save(os.path.join(UPLOAD_DIR, unique)) - saved.append(unique) - if saved: - flash(f"{len(saved)} image(s) uploaded. Copy the command shown under each image to use it in a template.", "ok") - if skipped: - flash(f"Skipped (not PNG/JPG/GIF/WEBP): {', '.join(skipped)}", "error") - return redirect(url_for("admin")) - - -@app.errorhandler(413) -def too_large(_e): - flash("Upload rejected: the total upload size exceeds the 15 MB limit.", "error") - return redirect(url_for("admin")) - - -@app.route("/images//delete", methods=["POST"]) -@login_required -def delete_image(fname): - path = os.path.join(UPLOAD_DIR, secure_filename(fname)) - if os.path.isfile(path): - os.remove(path) - flash("Image deleted.", "ok") - return redirect(url_for("admin")) - - -@app.route("/uploads/") -@login_required -def serve_upload(fname): - return send_from_directory(UPLOAD_DIR, secure_filename(fname)) - - -# ---------------------------------------------------------------- startup - -init_db() -scheduler = BackgroundScheduler(daemon=True) -_conn = db_connect() -_h, _m = get_send_time(_conn) -_conn.close() -scheduler.add_job(daily_birthday_job, "cron", hour=_h, minute=_m, - id="daily-birthday-mail") -scheduler.start() - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8080")), debug=False) diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/docker-compose.yml b/Martinhal Birthday Mailing App/v1.2/birthday-app/docker-compose.yml deleted file mode 100644 index 4be567d..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - birthday-manager: - build: . - container_name: birthday-manager - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - ./data:/data - environment: - # --- Purelymail account (fill in your mailbox credentials) --- - SMTP_USER: "you@yourdomain.com" - SMTP_PASS: "your-purelymail-password" - SMTP_FROM: "you@yourdomain.com" - - # --- Outgoing mail (pre-configured for Purelymail, SSL/TLS) --- - SMTP_HOST: "smtp.purelymail.com" - SMTP_PORT: "465" - SMTP_SECURITY: "ssl" # if your network only allows STARTTLS: port 587 + "starttls" - - # --- Incoming mail / Sent-folder copy (pre-configured for Purelymail) --- - IMAP_HOST: "imap.purelymail.com" - IMAP_PORT: "993" # SSL/TLS - # IMAP_USER / IMAP_PASS default to the SMTP credentials above - IMAP_SENT_FOLDER: "Sent" - - # --- optional --- - SEND_HOUR: "8" # default only - set the real time on the Administration page - SECRET_KEY: "change-this-to-a-long-random-string" - TZ: "Europe/Lisbon" diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/requirements.txt b/Martinhal Birthday Mailing App/v1.2/birthday-app/requirements.txt deleted file mode 100644 index 98270a0..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flask>=3.0 -pyotp>=2.9 -qrcode[pil]>=7.4 -APScheduler>=3.10 -werkzeug>=3.0 diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/static/style.css b/Martinhal Birthday Mailing App/v1.2/birthday-app/static/style.css deleted file mode 100644 index 32b7fd3..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/static/style.css +++ /dev/null @@ -1,258 +0,0 @@ -/* Birthday Manager — © 2026 Martinhal IT */ -:root { - --pine: #123f3a; - --pine-deep: #0c2b28; - --paper: #f6f4ee; - --card: #ffffff; - --ink: #21302d; - --muted: #6b7a76; - --line: #e2ddd1; - --marigold: #e8a13a; - --marigold-deep: #c07f1c; - --danger: #b3402a; - --ok: #2e7d4f; - font-size: 16px; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - min-height: 100vh; - display: flex; - flex-direction: column; - background: var(--paper); - color: var(--ink); - font-family: "Inter", "Segoe UI", system-ui, sans-serif; -} - -h1, h2, h3, .brand-name, .cal-year { - font-family: "Sora", "Segoe UI", system-ui, sans-serif; - letter-spacing: -0.01em; -} -h1 { font-size: 1.6rem; margin: 0.2rem 0; } -h2 { font-size: 1.15rem; margin-top: 0; } -h3 { font-size: 0.9rem; margin: 0 0 0.4rem; } - -/* ---------- top bar ---------- */ -.topbar { - display: flex; - align-items: center; - gap: 2rem; - padding: 0.7rem 1.4rem; - background: var(--pine); - color: #f2efe6; -} -.brand { display: flex; align-items: center; gap: 0.55rem; font-weight: 700; } -.brand-mark { font-size: 1.3rem; } -.nav { display: flex; gap: 0.4rem; flex: 1; } -.nav a { - color: #cfe0d8; - text-decoration: none; - padding: 0.45rem 0.9rem; - border-radius: 8px; - font-weight: 500; -} -.nav a:hover { background: rgba(255,255,255,0.08); color: #fff; } -.nav a.active { background: var(--marigold); color: var(--pine-deep); font-weight: 600; } -.userbox { display: flex; align-items: center; gap: 0.7rem; } -.userchip { - background: rgba(255,255,255,0.12); - padding: 0.3rem 0.75rem; - border-radius: 999px; - font-size: 0.85rem; -} - -/* ---------- layout ---------- */ -.page { - flex: 1; - width: min(1180px, 100%); - margin: 0 auto; - padding: 1.4rem 1.2rem 2.5rem; -} -.card { - background: var(--card); - border: 1px solid var(--line); - border-radius: 14px; - padding: 1.2rem 1.4rem; - margin-bottom: 1.3rem; - box-shadow: 0 1px 2px rgba(18, 63, 58, 0.05); -} -.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 1.3rem; } -.two-col .card { margin-bottom: 1.3rem; } -@media (max-width: 860px) { .two-col { grid-template-columns: 1fr; } } - -.footer { - text-align: center; - padding: 1rem; - background: var(--pine-deep); - color: #cfe0d8; - font-size: 0.85rem; -} - -/* ---------- yesterday / today / tomorrow banner ---------- */ -.tri-banner { - display: grid; - grid-template-columns: 1fr 1.6fr 1fr; - gap: 1.3rem; - align-items: stretch; -} -.day-panel { margin-bottom: 1.3rem; } -.day-panel-today { - border-left: 6px solid var(--marigold); - display: flex; - gap: 1rem; - align-items: center; - justify-content: space-between; -} -@media (max-width: 980px) { .tri-banner { grid-template-columns: 1fr; } } - -/* ---------- image copy command ---------- */ -.copy-row { display: flex; gap: 0.35rem; width: 100%; } -.copy-input { - flex: 1; - min-width: 0; - font-family: ui-monospace, "Cascadia Code", Consolas, monospace; - font-size: 0.72rem; - padding: 0.35rem 0.45rem; - background: #f0ede4; -} - -/* ---------- today banner ---------- */ -.today-banner { - display: flex; - gap: 1.5rem; - align-items: center; - justify-content: space-between; - border-left: 6px solid var(--marigold); -} -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.12em; - font-size: 0.72rem; - color: var(--muted); - margin: 0; -} -.today-people { list-style: none; margin: 0.4rem 0 0; padding: 0; } -.today-people li { margin: 0.25rem 0; } -.today-right { text-align: right; flex-shrink: 0; max-width: 220px; } - -/* ---------- forms ---------- */ -label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; font-weight: 500; } -input, select, textarea { - font: inherit; - padding: 0.5rem 0.65rem; - border: 1px solid var(--line); - border-radius: 8px; - background: #fdfcf9; -} -input:focus, select:focus, textarea:focus, button:focus-visible, a:focus-visible { - outline: 2px solid var(--marigold); - outline-offset: 1px; -} -textarea { resize: vertical; font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 0.85rem; } -.entry-form { - display: flex; - flex-wrap: wrap; - gap: 0.8rem; - align-items: flex-end; - margin-bottom: 1rem; -} -.entry-form label { flex: 1 1 150px; } -.stack-form { display: flex; flex-direction: column; gap: 0.8rem; align-items: flex-start; } -.stack-form label { width: 100%; } -.row-gap { display: flex; gap: 0.7rem; margin-top: 0.8rem; } - -/* ---------- buttons ---------- */ -.btn { - font: inherit; - font-weight: 600; - border: none; - border-radius: 9px; - padding: 0.55rem 1.1rem; - cursor: pointer; - text-decoration: none; - display: inline-block; -} -.btn-primary { background: var(--pine); color: #fff; } -.btn-primary:hover { background: var(--pine-deep); } -.btn-accent { background: var(--marigold); color: var(--pine-deep); } -.btn-accent:hover { background: var(--marigold-deep); color: #fff; } -.btn-danger { background: #fbeae5; color: var(--danger); } -.btn-danger:hover { background: var(--danger); color: #fff; } -.btn-ghost { background: transparent; color: inherit; border: 1px solid currentColor; opacity: 0.85; } -.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.8rem; } -.btn-lg { padding: 0.8rem 1.6rem; font-size: 1.05rem; } -.btn-block { width: 100%; } -.btn[disabled] { opacity: 0.45; cursor: not-allowed; } - -/* ---------- tables ---------- */ -.table-wrap { overflow-x: auto; } -table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } -th { text-align: left; color: var(--muted); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; } -th, td { padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--line); } -tbody tr:hover { background: #faf8f2; } -.ta-right { text-align: right; } -.nowrap { white-space: nowrap; } -.log-wrap { max-height: 420px; overflow-y: auto; } - -/* ---------- pills / flash ---------- */ -.pill { border-radius: 999px; padding: 0.15rem 0.6rem; font-size: 0.72rem; font-weight: 600; } -.pill-ok { background: #e2f2e8; color: var(--ok); } -.pill-bad { background: #fbeae5; color: var(--danger); } -.pill-wait { background: #fdf1dc; color: var(--marigold-deep); } -.flash { padding: 0.7rem 1rem; border-radius: 10px; margin-bottom: 1rem; font-size: 0.9rem; } -.flash-ok { background: #e2f2e8; color: var(--ok); } -.flash-error { background: #fbeae5; color: var(--danger); } - -/* ---------- yearly calendar ---------- */ -.cal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; } -.cal-nav { display: flex; align-items: center; gap: 0.7rem; } -.cal-year { font-size: 1.2rem; font-weight: 700; } -.year-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); - gap: 1rem; -} -.mini-cal { font-size: 0.72rem; } -.mini-cal th, .mini-cal td { padding: 0.22rem; text-align: center; border: none; } -.mini-cal td { position: relative; border-radius: 6px; } -.mini-cal td.bday { background: #fdf1dc; font-weight: 700; color: var(--marigold-deep); cursor: help; } -.mini-cal td.today { outline: 2px solid var(--pine); } -.mini-cal .dot { - position: absolute; bottom: 1px; left: 50%; transform: translateX(-50%); - width: 4px; height: 4px; border-radius: 50%; background: var(--marigold-deep); -} - -/* ---------- images ---------- */ -.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-top: 1rem; } -.image-tile { - border: 1px solid var(--line); border-radius: 10px; padding: 0.6rem; - display: flex; flex-direction: column; gap: 0.5rem; align-items: center; text-align: center; -} -.image-tile img { max-width: 100%; max-height: 110px; object-fit: contain; border-radius: 6px; } -.image-tile code { word-break: break-all; } - -/* ---------- auth / misc ---------- */ -.auth-card { - background: var(--card); border: 1px solid var(--line); border-radius: 14px; - max-width: 380px; margin: 8vh auto 0; padding: 2rem; - display: flex; flex-direction: column; gap: 0.4rem; -} -.auth-card.wide { max-width: 480px; margin-top: 3vh; } -.auth-card form { display: flex; flex-direction: column; gap: 0.9rem; margin-top: 0.8rem; } -.qr-wrap { text-align: center; margin: 0.5rem 0; } -.qr-wrap img { width: 190px; height: 190px; } -.muted { color: var(--muted); } -.small { font-size: 0.8rem; } -code { background: #f0ede4; padding: 0.1rem 0.35rem; border-radius: 5px; font-size: 0.82em; } -.tpl-zone textarea { width: 100%; } - -@media (prefers-reduced-motion: no-preference) { - .btn { transition: background 0.15s ease, color 0.15s ease; } -} -@media (max-width: 700px) { - .topbar { flex-wrap: wrap; gap: 0.6rem; } - .today-banner { flex-direction: column; align-items: flex-start; } - .today-right { text-align: left; } -} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/admin.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/admin.html deleted file mode 100644 index 903d0a9..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/admin.html +++ /dev/null @@ -1,165 +0,0 @@ -{% extends "base.html" %} -{% set active = 'admin' %} -{% block title %}Administration - Birthday Manager{% endblock %} -{% block content %} - - -
    -

    Users

    -
    - - - -
    -
    - - - - {% for u in users %} - - - - - - - {% endfor %} - -
    UsernameMFACreated
    {{ u.username }}{% if u.id == user_id %} you{% endif %}{{ 'Enabled' if u.mfa_enabled else 'Off' }}{{ u.created_at }} - {% if u.id != user_id %} -
    - -
    - {% endif %} -
    -
    -
    - - -
    -
    -

    Change my password

    -
    - - - - -
    -
    - -
    -

    Two-factor authentication (MFA)

    - {% set me = users | selectattr('id', 'equalto', user_id) | first %} - {% if me and me.mfa_enabled %} -

    MFA is enabled for your account. You'll be asked for a 6-digit code every time you sign in.

    -
    - -
    - {% else %} -

    Protect your account with a one-time code from an authenticator app (Google Authenticator, Authy, Microsoft Authenticator…).

    - Enable MFA - {% endif %} -
    -
    - - -
    -

    Automatic e-mail send time

    -

    The birthday e-mails are sent automatically every day at this time.

    -
    - - -
    -
    - - -
    -

    E-mail log

    - {% if log %} -
    - - - - {% for l in log %} - - - - - - - - {% endfor %} - -
    Date & timeRecipientE-mail addressSubjectStatus
    {{ l.sent_at }}{{ l.recipient_name }}{{ l.recipient_email }}{{ l.subject }} - {{ l.status }} -
    -
    - {% else %} -

    No e-mails have been sent yet.

    - {% endif %} -
    - - -
    -

    Images

    -

    Upload one or more images (15 MB total per upload). Each image shows the ready-to-use - <img> command — copy it into a template and the image is embedded inline in the e-mail when it is sent.

    -
    - - -
    - {% if images %} -
    - {% for img in images %} -
    - {{ img }} -
    - - -
    -
    - -
    -
    - {% endfor %} -
    - {% endif %} -
    - - -
    -
    -

    E-mail Template Male

    -
    - - - -

    Placeholders: {first_name} {last_name} {age} — insert images with <img src="/uploads/…">

    - -
    -
    - -
    -

    E-Mail Template Female

    -
    - - - -

    Placeholders: {first_name} {last_name} {age} — insert images with <img src="/uploads/…">

    - -
    -
    -
    - -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/base.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/base.html deleted file mode 100644 index 522cb7c..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/base.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - {% block title %}Birthday Manager{% endblock %} - - - - - -
    -
    - 🎂 - Birthday Manager -
    - {% if username %} - -
    - {{ username }} - Log out -
    - {% endif %} -
    - -
    - {% with messages = get_flashed_messages(with_categories=true) %} - {% for category, msg in messages %} -
    {{ msg }}
    - {% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
    - -
    {{ footer }}
    - - diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/dashboard.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/dashboard.html deleted file mode 100644 index 710cea8..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/dashboard.html +++ /dev/null @@ -1,147 +0,0 @@ -{% extends "base.html" %} -{% set active = 'dashboard' %} -{% block title %}Dashboard - Birthday Manager{% endblock %} -{% block content %} - - -
    -
    -

    Yesterday

    -

    Yesterday's Birthdays

    - {% if yesterday_list %} -
      - {% for item in yesterday_list %} -
    • {{ item.row.first_name }} {{ item.row.last_name }} turned {{ item.age }}
    • - {% endfor %} -
    - {% else %} -

    No birthdays yesterday.

    - {% endif %} -
    - -
    -
    -

    Today, {{ today.strftime('%A %d %B %Y') }}

    -

    Today's Birthdays

    - {% if today_list %} -
      - {% for item in today_list %} -
    • - 🎉 {{ item.row.first_name }} {{ item.row.last_name }} - turns {{ item.age }} - - {{ 'e-mail sent' if item.sent else 'e-mail pending' }} - -
    • - {% endfor %} -
    - {% else %} -

    No birthdays today.

    - {% endif %} -
    -
    - -

    Sends today's birthday e-mail{{ 's' if today_list|length != 1 }} again immediately.

    -
    -
    - -
    -

    Tomorrow

    -

    Tomorrow's Birthdays

    - {% if tomorrow_list %} -
      - {% for item in tomorrow_list %} -
    • {{ item.row.first_name }} {{ item.row.last_name }} turns {{ item.age }}
    • - {% endfor %} -
    - {% else %} -

    No birthdays tomorrow.

    - {% endif %} -
    -
    - - -
    -

    Birthday entries

    -
    - - - - - - -
    - - {% if entries %} -
    - - - - - - {% for e in entries %} - - - - - - - - - {% endfor %} - -
    First NameLast NameDate of BirthGenderE-mail
    {{ e.first_name }}{{ e.last_name }}{{ e.dob }}{{ e.gender }}{{ e.email }} -
    - -
    -
    -
    - {% else %} -

    No entries yet. Add the first birthday above.

    - {% endif %} -
    - - -
    -
    -

    Yearly overview

    - -
    -
    - {% for month in months %} -
    -

    {{ month.name }}

    - - - - {% for week in month.weeks %} - - {% for cell in week %} - - {% endfor %} - - {% endfor %} - -
    MoTuWeThFrSaSu
    - {{ cell.day }} - {% if cell.names %}{% endif %} -
    -
    - {% endfor %} -
    -

    Days marked with a dot have a birthday — hover to see who. Today is outlined.

    -
    - -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/login.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/login.html deleted file mode 100644 index 9a8afa3..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/login.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} -{% block title %}Sign in - Birthday Manager{% endblock %} -{% block content %} -
    -

    Sign in

    -

    Birthday Manager

    -
    - - - -
    -
    -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_setup.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_setup.html deleted file mode 100644 index 9c3e3f0..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_setup.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "base.html" %} -{% set active = 'admin' %} -{% block title %}Enable two-factor authentication{% endblock %} -{% block content %} -
    -

    Enable two-factor authentication

    -

    Scan this QR code with Google Authenticator, Microsoft Authenticator, Authy or any TOTP app, then enter the 6-digit code it shows to confirm.

    -
    - MFA QR code -

    Can't scan? Enter this key manually: {{ secret }}

    -
    -
    - -
    - - Cancel -
    -
    -
    -{% endblock %} diff --git a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_verify.html b/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_verify.html deleted file mode 100644 index d0cdae8..0000000 --- a/Martinhal Birthday Mailing App/v1.2/birthday-app/templates/mfa_verify.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} -{% block title %}Two-factor authentication{% endblock %} -{% block content %} -
    -

    Two-factor code

    -

    Enter the 6-digit code from your authenticator app.

    -
    - - -
    -
    -{% endblock %} diff --git a/Martinhal Contacts/v1.2/Martinhal_Contact_Site.zip b/Martinhal Contacts/v1.2/Martinhal_Contact_Site.zip deleted file mode 100644 index 63fc2e3..0000000 Binary files a/Martinhal Contacts/v1.2/Martinhal_Contact_Site.zip and /dev/null differ diff --git a/Martinhal Contacts/v1.3/Martinhal_Contact_Site_v1.3_update.zip b/Martinhal Contacts/v1.3/Martinhal_Contact_Site_v1.3_update.zip deleted file mode 100644 index f2278ae..0000000 Binary files a/Martinhal Contacts/v1.3/Martinhal_Contact_Site_v1.3_update.zip and /dev/null differ diff --git a/Martinhal Contacts/v1.4/Martinhal_Contact_Site_v1.4_update.zip b/Martinhal Contacts/v1.4/Martinhal_Contact_Site_v1.4_update.zip deleted file mode 100644 index f3411b9..0000000 Binary files a/Martinhal Contacts/v1.4/Martinhal_Contact_Site_v1.4_update.zip and /dev/null differ diff --git a/Martinhal Contacts/v1.5/Martinhal_Contact_Site_v1.5_update.zip b/Martinhal Contacts/v1.5/Martinhal_Contact_Site_v1.5_update.zip deleted file mode 100644 index 2ba16dd..0000000 Binary files a/Martinhal Contacts/v1.5/Martinhal_Contact_Site_v1.5_update.zip and /dev/null differ diff --git a/Martinhal_Contact_Site/Martinhal_Contact_Site.7z b/Martinhal_Contact_Site/Martinhal_Contact_Site.7z deleted file mode 100644 index 90d4f6f..0000000 Binary files a/Martinhal_Contact_Site/Martinhal_Contact_Site.7z and /dev/null differ diff --git a/Martinhal_Futebol b/Martinhal_Futebol deleted file mode 160000 index 76f2ee9..0000000 --- a/Martinhal_Futebol +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 76f2ee989f33f57587865b34d14b70449e03d88d diff --git a/Martinhal_Mobile_Contracts b/Martinhal_Mobile_Contracts deleted file mode 160000 index 4dea02c..0000000 --- a/Martinhal_Mobile_Contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4dea02c3c7d1b92532ab92011d2d4a7fd705c17c diff --git a/Infosec/OLD_VERSIONS/infosec-v1_1.zip b/OLD_VERSIONS/infosec-v1_1.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v1_1.zip rename to OLD_VERSIONS/infosec-v1_1.zip diff --git a/Infosec/OLD_VERSIONS/infosec-v1_2.zip b/OLD_VERSIONS/infosec-v1_2.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v1_2.zip rename to OLD_VERSIONS/infosec-v1_2.zip diff --git a/Infosec/OLD_VERSIONS/infosec-v2_0.zip b/OLD_VERSIONS/infosec-v2_0.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v2_0.zip rename to OLD_VERSIONS/infosec-v2_0.zip diff --git a/Infosec/OLD_VERSIONS/infosec-v2_1.zip b/OLD_VERSIONS/infosec-v2_1.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v2_1.zip rename to OLD_VERSIONS/infosec-v2_1.zip diff --git a/Infosec/OLD_VERSIONS/infosec-v2_2.zip b/OLD_VERSIONS/infosec-v2_2.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v2_2.zip rename to OLD_VERSIONS/infosec-v2_2.zip diff --git a/Infosec/OLD_VERSIONS/infosec-v2_3.zip b/OLD_VERSIONS/infosec-v2_3.zip similarity index 100% rename from Infosec/OLD_VERSIONS/infosec-v2_3.zip rename to OLD_VERSIONS/infosec-v2_3.zip diff --git a/Presentation system/v1.1/files.zip b/Presentation system/v1.1/files.zip deleted file mode 100644 index 5602499..0000000 Binary files a/Presentation system/v1.1/files.zip and /dev/null differ diff --git a/Presentation system/v1.2/qr-presentation.zip b/Presentation system/v1.2/qr-presentation.zip deleted file mode 100644 index 7d149f5..0000000 Binary files a/Presentation system/v1.2/qr-presentation.zip and /dev/null differ diff --git a/Presentation system/v1.3/MPS_logo.png b/Presentation system/v1.3/MPS_logo.png deleted file mode 100644 index 59dd6a0..0000000 Binary files a/Presentation system/v1.3/MPS_logo.png and /dev/null differ diff --git a/Presentation system/v1.3/README.md b/Presentation system/v1.3/README.md deleted file mode 100644 index 9949e52..0000000 --- a/Presentation system/v1.3/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# MPS — Martinhal Presentation System - -A self-hosted app that turns any presentation into a link and a QR code. Upload a -**video, PowerPoint, Word doc, Excel sheet, PDF, or image**; the app converts it to a -browser-friendly format and generates a QR code. Anyone who scans it watches along on -their own phone — **live and in sync with the presenter**, or browsing at their own pace. - -Everything runs in a single Docker container. No cloud, no third-party services except -the mail account you choose to connect for email sharing. - -**Version 1.3** · © 2026 Martinhal IT — Joao Vaz - ---- - -## What's new in 1.3 - -- **First-run setup.** The very first time you open the app it asks you to **create the - administrator account** — no default password to remember or change. -- **Rebranded** as MPS — Martinhal Presentation System, with the MPS logo across the app. -- **QR Code button** on every ready presentation opens the code on its own page in - **large format** — ideal for putting on a projector. It has copy-link, fullscreen and - print actions. -- **Bigger presentation cards** on the host dashboard (twice as wide, 50% taller) so the - QR preview and controls are easier to use. - -### From 1.2 - -Login page and user accounts, admin-only user management, mail settings and activity logs, -email sharing, PurelyMail SMTP+IMAP with a copy saved to **Sent**, and First/Last slide -controls. - ---- - -## Quick start (Docker Compose) - -```bash -# Point QR codes at an address phones can actually reach (LAN IP or public URL). -export PUBLIC_BASE_URL=http://192.168.1.50:3000 - -docker compose up --build -d -``` - -Open `http://:3000`. On first launch you'll be asked to **create the admin -account**. After that, sign in, upload a file, and hit **Present**. Show the QR code -(**Share / QR**, or the **QR Code** button for a full-screen page) and the room scans to -join. - -> **Unattended installs:** if you'd rather not use the setup screen, set `ADMIN_USERNAME` -> and `ADMIN_PASSWORD` before the first start and an admin will be pre-created from those -> values instead. They're ignored once an account exists. - -### Plain Docker - -```bash -docker build -t mps . -docker run -d --name mps \ - -p 3000:3000 \ - -e PUBLIC_BASE_URL=http://192.168.1.50:3000 \ - -v qr_data:/data \ - mps -``` - -### Without Docker (Node 18+) - -Requires `libreoffice`, `ffmpeg`, and fonts on the host. - -```bash -npm install -npm run fetch-vendor # vendors PDF.js for offline use -npm start -``` - ---- - -## Updating (no data loss) - -All state — user accounts, mail settings, logs, uploaded decks — lives under the `/data` -volume, never inside the app code. To update: - -1. Unzip the new version over your existing project folder (code files are replaced; - your `data/` and the Docker `qr_data` volume are left untouched). -2. Rebuild and restart: - - ```bash - docker compose up --build -d - ``` - -Your admin account, users, presentations, and mail configuration carry straight over, and -the first-run setup screen does **not** reappear. - ---- - -## Users & roles - -| Role | Can do | -|---|---| -| **admin** | Everything: manage users, configure mail, view logs, see and manage **all** presentations. | -| **user** | Sign in, upload and present **their own** decks, share them (link, QR, email). | - -The first admin is created during setup. Manage further accounts at **/users** (admins -only). The app prevents removing or demoting the last remaining admin, and you can't -delete your own account. - ---- - -## Sharing a presentation - -Each ready presentation offers several ways to get people in: - -- **QR Code** — opens a dedicated full-screen page with a large, scannable code (plus - copy-link, fullscreen, and print). Great for a projector or printed handout. -- **Share / QR** (in the presenter view) — a quick pop-over with the code and link. -- **Copy link** — the plain audience URL. -- **Email** — sends the link and QR straight to recipients (requires mail set up). - ---- - -## Email sharing (PurelyMail) - -1. Sign in as an admin → **Manage users → Mail**. -2. Tick **Enable email sending**, enter the PurelyMail **account email** and **password**. - Servers are prefilled and match PurelyMail's settings: - - | | Host | Port | Security | - |---|---|---|---| - | SMTP (send) | `smtp.purelymail.com` | `465` | SSL/TLS | - | IMAP (Sent copy) | `imap.purelymail.com` | `993` | SSL/TLS | - -3. Save, then **Send test** to confirm it works. - -Every message is delivered over SMTP and a copy is appended to the **Sent** folder over -IMAP, so it appears in your normal mail client. The stored mail password lives only in -`/data/config.json` on your server and is never sent back to the browser. - ---- - -## Presenting - -- **Slides:** `« First` · `‹ Prev` · `Next ›` · `Last »`, or the keys `←` `→` `Home` `End`. -- **Synced ↔ Free browse:** in Synced mode the audience follows you; in Free browse they - move on their own. Audience members who wander off get a one-tap "Rejoin presenter". -- **Video:** play / pause / seek are broadcast to everyone. Viewers tap once to allow - sound (browsers block autoplay audio until then). - ---- - -## Supported formats - -| Input | Handled as | Tool | -|---|---|---| -| `.pdf` | paged deck | served directly | -| `.pptx .ppt .odp .pps .ppsx` | paged deck | LibreOffice → PDF | -| `.docx .doc .odt .rtf .txt .md` | paged deck | LibreOffice → PDF | -| `.xlsx .xls .ods .csv .tsv` | paged deck | LibreOffice → PDF | -| `.mp4 .webm .ogg` | synced video | served directly | -| `.mov .avi .mkv .wmv .flv .3gp .mpeg .ts` | synced video | ffmpeg → MP4 | -| `.png .jpg .jpeg .gif .webp .svg .bmp .avif` | image | served directly | - ---- - -## Configuration - -| Variable | Default | Purpose | -|---|---|---| -| `PUBLIC_BASE_URL` | derived from request | Absolute URL encoded in QR codes / links. **Set this.** | -| `ADMIN_USERNAME` | `admin` | Only for unattended pre-seed (see Quick start). | -| `ADMIN_PASSWORD` | *(unset)* | If set on first run, pre-creates an admin instead of showing setup. | -| `ADMIN_EMAIL` | *(empty)* | Email for a pre-seeded admin. | -| `MAX_UPLOAD_MB` | `500` | Maximum upload size. | -| `PORT` | `3000` | Listen port. | -| `DATA_DIR` | `/data` (Docker) | Where all state is stored. Mount as a volume. | -| `CONVERT_TIMEOUT_MS` | `240000` | Per-file conversion timeout. | - ---- - -## Security notes - -- Put a reverse proxy with **HTTPS** in front for anything beyond a trusted LAN, and set - `PUBLIC_BASE_URL` to the https address. -- Passwords are stored as salted scrypt hashes. Login sessions are HttpOnly cookies. -- The container runs as a non-root user; all state stays on the mounted volume. - -## License - -MIT. diff --git a/Presentation system/v1.3/qr-presentation.zip b/Presentation system/v1.3/qr-presentation.zip deleted file mode 100644 index 475e255..0000000 Binary files a/Presentation system/v1.3/qr-presentation.zip and /dev/null differ diff --git a/Presentation system/v1.4/qr-presentation.zip b/Presentation system/v1.4/qr-presentation.zip deleted file mode 100644 index 7c04950..0000000 Binary files a/Presentation system/v1.4/qr-presentation.zip and /dev/null differ diff --git a/Presentation system/v1.5/qr-presentation.zip b/Presentation system/v1.5/qr-presentation.zip deleted file mode 100644 index 761e42c..0000000 Binary files a/Presentation system/v1.5/qr-presentation.zip and /dev/null differ diff --git a/Presentation system/v1.6/qr-presentation.zip b/Presentation system/v1.6/qr-presentation.zip deleted file mode 100644 index 9da79f6..0000000 Binary files a/Presentation system/v1.6/qr-presentation.zip and /dev/null differ diff --git a/Presentation system/v1.7/qr-presentation.zip b/Presentation system/v1.7/qr-presentation.zip deleted file mode 100644 index 160c143..0000000 Binary files a/Presentation system/v1.7/qr-presentation.zip and /dev/null differ diff --git a/Infosec/README.md b/README.md similarity index 100% rename from Infosec/README.md rename to README.md diff --git a/Self-Hosted Stream Webcam/files.zip b/Self-Hosted Stream Webcam/files.zip deleted file mode 100644 index eef7865..0000000 Binary files a/Self-Hosted Stream Webcam/files.zip and /dev/null differ diff --git a/Infosec/UPGRADE.md b/UPGRADE.md similarity index 100% rename from Infosec/UPGRADE.md rename to UPGRADE.md diff --git a/WikiJS/wikiJS.zip b/WikiJS/wikiJS.zip deleted file mode 100644 index df9f10f..0000000 Binary files a/WikiJS/wikiJS.zip and /dev/null differ diff --git a/Infosec/avatars/.gitkeep b/avatars/.gitkeep similarity index 100% rename from Infosec/avatars/.gitkeep rename to avatars/.gitkeep diff --git a/Infosec/brand/.gitkeep b/brand/.gitkeep similarity index 100% rename from Infosec/brand/.gitkeep rename to brand/.gitkeep diff --git a/Infosec/data/.gitkeep b/data/.gitkeep similarity index 100% rename from Infosec/data/.gitkeep rename to data/.gitkeep diff --git a/Infosec/db.js b/db.js similarity index 100% rename from Infosec/db.js rename to db.js diff --git a/Infosec/docker-compose.yml b/docker-compose.yml similarity index 100% rename from Infosec/docker-compose.yml rename to docker-compose.yml diff --git a/Infosec/docker-entrypoint.sh b/docker-entrypoint.sh similarity index 100% rename from Infosec/docker-entrypoint.sh rename to docker-entrypoint.sh diff --git a/Infosec/init-db.js b/init-db.js similarity index 100% rename from Infosec/init-db.js rename to init-db.js diff --git a/Infosec/lib/audit.js b/lib/audit.js similarity index 100% rename from Infosec/lib/audit.js rename to lib/audit.js diff --git a/Infosec/lib/dates.js b/lib/dates.js similarity index 100% rename from Infosec/lib/dates.js rename to lib/dates.js diff --git a/Infosec/lib/mailer.js b/lib/mailer.js similarity index 100% rename from Infosec/lib/mailer.js rename to lib/mailer.js diff --git a/Infosec/lib/settings.js b/lib/settings.js similarity index 100% rename from Infosec/lib/settings.js rename to lib/settings.js diff --git a/Infosec/lib/templates.js b/lib/templates.js similarity index 100% rename from Infosec/lib/templates.js rename to lib/templates.js diff --git a/Infosec/lib/validate.js b/lib/validate.js similarity index 100% rename from Infosec/lib/validate.js rename to lib/validate.js diff --git a/Infosec/middleware/auth.js b/middleware/auth.js similarity index 100% rename from Infosec/middleware/auth.js rename to middleware/auth.js diff --git a/Infosec/package-lock.json b/package-lock.json similarity index 100% rename from Infosec/package-lock.json rename to package-lock.json diff --git a/Infosec/package.json b/package.json similarity index 100% rename from Infosec/package.json rename to package.json diff --git a/Infosec/public/account.html b/public/account.html similarity index 100% rename from Infosec/public/account.html rename to public/account.html diff --git a/Infosec/public/assets/infosec-logo.png b/public/assets/infosec-logo.png similarity index 100% rename from Infosec/public/assets/infosec-logo.png rename to public/assets/infosec-logo.png diff --git a/Infosec/public/calendar-view.html b/public/calendar-view.html similarity index 100% rename from Infosec/public/calendar-view.html rename to public/calendar-view.html diff --git a/Infosec/public/css/style.css b/public/css/style.css similarity index 100% rename from Infosec/public/css/style.css rename to public/css/style.css diff --git a/Infosec/public/data-management.html b/public/data-management.html similarity index 100% rename from Infosec/public/data-management.html rename to public/data-management.html diff --git a/Infosec/public/js/app.js b/public/js/app.js similarity index 100% rename from Infosec/public/js/app.js rename to public/js/app.js diff --git a/Infosec/public/legislation.html b/public/legislation.html similarity index 100% rename from Infosec/public/legislation.html rename to public/legislation.html diff --git a/Infosec/public/login.html b/public/login.html similarity index 100% rename from Infosec/public/login.html rename to public/login.html diff --git a/Infosec/public/logs.html b/public/logs.html similarity index 100% rename from Infosec/public/logs.html rename to public/logs.html diff --git a/Infosec/public/setup.html b/public/setup.html similarity index 100% rename from Infosec/public/setup.html rename to public/setup.html diff --git a/Infosec/public/storage.html b/public/storage.html similarity index 100% rename from Infosec/public/storage.html rename to public/storage.html diff --git a/Infosec/public/version-control.html b/public/version-control.html similarity index 100% rename from Infosec/public/version-control.html rename to public/version-control.html diff --git a/Infosec/public/view-data.html b/public/view-data.html similarity index 100% rename from Infosec/public/view-data.html rename to public/view-data.html diff --git a/Infosec/routes/auth.js b/routes/auth.js similarity index 100% rename from Infosec/routes/auth.js rename to routes/auth.js diff --git a/Infosec/routes/data.js b/routes/data.js similarity index 100% rename from Infosec/routes/data.js rename to routes/data.js diff --git a/Infosec/routes/datacreate.js b/routes/datacreate.js similarity index 100% rename from Infosec/routes/datacreate.js rename to routes/datacreate.js diff --git a/Infosec/routes/legislation.js b/routes/legislation.js similarity index 100% rename from Infosec/routes/legislation.js rename to routes/legislation.js diff --git a/Infosec/routes/logs.js b/routes/logs.js similarity index 100% rename from Infosec/routes/logs.js rename to routes/logs.js diff --git a/Infosec/routes/mail.js b/routes/mail.js similarity index 100% rename from Infosec/routes/mail.js rename to routes/mail.js diff --git a/Infosec/routes/manage.js b/routes/manage.js similarity index 100% rename from Infosec/routes/manage.js rename to routes/manage.js diff --git a/Infosec/routes/settings.js b/routes/settings.js similarity index 100% rename from Infosec/routes/settings.js rename to routes/settings.js diff --git a/Infosec/routes/setup.js b/routes/setup.js similarity index 100% rename from Infosec/routes/setup.js rename to routes/setup.js diff --git a/Infosec/routes/storage.js b/routes/storage.js similarity index 100% rename from Infosec/routes/storage.js rename to routes/storage.js diff --git a/Infosec/routes/users.js b/routes/users.js similarity index 100% rename from Infosec/routes/users.js rename to routes/users.js diff --git a/Infosec/routes/versions.js b/routes/versions.js similarity index 100% rename from Infosec/routes/versions.js rename to routes/versions.js diff --git a/Infosec/server.js b/server.js similarity index 100% rename from Infosec/server.js rename to server.js diff --git a/Infosec/uploads/.gitkeep b/uploads/.gitkeep similarity index 100% rename from Infosec/uploads/.gitkeep rename to uploads/.gitkeep