7 Commits

Author SHA1 Message Date
mfwadejr caf98ed6a9 Disable buildx provenance/SBOM attestations for the published image
Publish container / publish (push) Waiting to run
QEMU cross-arch emulation hung indefinitely on the self-hosted Gitea
runner (likely no privileged Docker access), so the actual arch wasn't
the problem worth chasing. The far more common cause of "no matching
manifest" against a self-hosted registry is Buildx's default
provenance/SBOM attestation manifests, which not every registry
implementation (including Gitea's) resolves correctly when a client
requests a single-platform pull. Disabling both keeps the image a
plain single-manifest image, matching what a manual `docker push`
already produced and pulled fine.
2026-09-21 22:58:24 +00:00
mfwadejr 3e31721b0e Build multi-arch images so pulls work regardless of runner/host CPU
Publish container / publish (push) Has been cancelled
The first successful Gitea-side publish only produced a manifest for
whatever architecture the self-hosted Actions runner happens to be,
which didn't match the amd64 ZimaOS host pulling it ("no matching
manifest for linux/amd64"). Adds QEMU + Buildx setup and builds for
linux/amd64 and linux/arm64 explicitly so the published image works on
any host regardless of what CPU built it.
2026-09-21 22:54:18 +00:00
mfwadejr 2a9d95d3d9 Show running build version in the UI so stale deploys are obvious
Publish container / publish (push) Successful in 24s
Bakes the git commit SHA into the image at build time (Dockerfile
GIT_SHA/BUILD_DATE build-args, set by the CI workflow) and surfaces it
as a small "vX.Y.Z · <sha>" tag on the sign-in screen and in the header
user-menu, sourced from the existing unauthenticated /api/health
endpoint (now also returns appVersion/buildSha/buildDate). Also adds
buildSha/buildDate to the admin diagnostics payload. This makes it
possible to tell at a glance whether a running container matches the
latest published image without digging through registry timestamps.
2026-09-21 22:42:53 +00:00
mfwadejr afa8da647f Use a package-scoped token for Gitea's container registry login
Publish container / publish (push) Successful in 49s
Gitea's own auto-injected GITHUB_TOKEN isn't authorized to write
packages on this instance even with packages:write declared, so the
Gitea-side publish kept failing at login despite targeting the right
registry. Added a REGISTRY_TOKEN repo secret (Gitea PAT scoped to
package: read/write only) and use it when the runner is Gitea; GitHub
keeps using its own real GITHUB_TOKEN for ghcr.io.
2026-09-21 22:35:37 +00:00
mfwadejr 05d24933f1 Fix container publish workflow to target the right registry per runner
Publish container / publish (push) Failing after 22s
The compose files were switched to pull from the Gitea registry
(git.us2plus2.com/marvin/vboxstock), but publish-container.yml was left
hardcoded to ghcr.io. Gitea Actions also executes .github/workflows, so
every push ran this job twice: it succeeded on GitHub (real
GITHUB_TOKEN, publishing to ghcr.io, which nothing consumes) and failed
on Gitea every time (Gitea's own GITHUB_TOKEN isn't valid for
github.com's registry). Now the registry/image target is derived from
github.server_url, so each runner logs into and publishes to its own
registry with its own automatic token.
2026-09-21 22:28:30 +00:00
mfwadejr f48005f219 Standardize control sizing with CSS tokens; fix header/page alignment
Publish container / publish (push) Failing after 22s
Introduces shared --control-h/--control-h-sm design tokens for button,
input, select, and textarea sizing so header, filter bar, forms, and
list controls stay visually consistent instead of drifting per-rule
padding math. Wraps header content in .header-inner sized like main
so the top bar aligns with the page content column instead of running
edge-to-edge on wide screens. Updates the 820px/760px mobile layouts
to target .header-inner instead of header directly.
2026-09-21 21:21:58 +00:00
mfwadejr 8ffcf30e97 Move container image references to Gitea registry
Publish container / publish (push) Failing after 10s
2026-09-21 11:45:27 -04:00
11 changed files with 53 additions and 28 deletions
+24 -8
View File
@@ -6,10 +6,6 @@ on:
tags: ["v*"] tags: ["v*"]
workflow_dispatch: workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: mfwadejr/vboxstock
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,18 +17,33 @@ jobs:
- name: Check out source - name: Check out source
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Sign in to GitHub Container Registry - name: Determine target registry
id: registry
run: |
if [ "${{ github.server_url }}" = "https://github.com" ]; then
echo "registry=ghcr.io" >> "$GITHUB_OUTPUT"
echo "image=ghcr.io/mfwadejr/vboxstock" >> "$GITHUB_OUTPUT"
else
host="$(printf '%s' '${{ github.server_url }}' | sed -E 's#^https?://##')"
echo "registry=$host" >> "$GITHUB_OUTPUT"
echo "image=$host/marvin/vboxstock" >> "$GITHUB_OUTPUT"
fi
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Sign in to container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ steps.registry.outputs.registry }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ github.server_url == 'https://github.com' && secrets.GITHUB_TOKEN || secrets.REGISTRY_TOKEN }}
- name: Generate image tags - name: Generate image tags
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ steps.registry.outputs.image }}
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag type=ref,event=tag
@@ -43,5 +54,10 @@ jobs:
with: with:
context: . context: .
push: true push: true
provenance: false
sbom: false
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
BUILD_DATE=${{ github.event.repository.updated_at || github.run_id }}
+6 -1
View File
@@ -1,5 +1,8 @@
FROM node:22-alpine FROM node:22-alpine
LABEL org.opencontainers.image.source="https://github.com/mfwadejr/vboxstock" ARG GIT_SHA=dev
ARG BUILD_DATE
LABEL org.opencontainers.image.source="https://git.us2plus2.com/marvin/vboxstock"
LABEL org.opencontainers.image.revision="$GIT_SHA"
LABEL org.opencontainers.image.description="Self-contained vSeeBox inventory and sales tracker" LABEL org.opencontainers.image.description="Self-contained vSeeBox inventory and sales tracker"
WORKDIR /app WORKDIR /app
COPY package.json server.mjs ./ COPY package.json server.mjs ./
@@ -9,6 +12,8 @@ RUN apk add --no-cache su-exec \
&& chmod +x /usr/local/bin/vboxstock-entrypoint \ && chmod +x /usr/local/bin/vboxstock-entrypoint \
&& mkdir -p /data && mkdir -p /data
ENV PORT=3000 DATA_DIR=/data NODE_ENV=production PUID=99 PGID=100 ENV PORT=3000 DATA_DIR=/data NODE_ENV=production PUID=99 PGID=100
ENV GIT_SHA=$GIT_SHA
ENV BUILD_DATE=$BUILD_DATE
EXPOSE 3000 EXPOSE 3000
VOLUME ["/data"] VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -q -O /dev/null http://127.0.0.1:3000/api/health || exit 1 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -q -O /dev/null http://127.0.0.1:3000/api/health || exit 1
+1 -1
View File
@@ -2,7 +2,7 @@ name: vboxstock
services: services:
vboxstock: vboxstock:
image: ghcr.io/mfwadejr/vboxstock:latest image: git.us2plus2.com/marvin/vboxstock:latest
container_name: vboxstock container_name: vboxstock
restart: unless-stopped restart: unless-stopped
ports: ports:
+2 -2
View File
@@ -2,7 +2,7 @@ name: vboxstock
services: services:
vboxstock: vboxstock:
image: ghcr.io/mfwadejr/vboxstock:latest image: git.us2plus2.com/marvin/vboxstock:latest
container_name: vboxstock container_name: vboxstock
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -24,7 +24,7 @@ x-casaos:
index: / index: /
port_map: "8269" port_map: "8269"
scheme: http scheme: http
icon: https://raw.githubusercontent.com/mfwadejr/vboxstock/main/public/assets/vboxstock-icon-512.png icon: https://git.us2plus2.com/marvin/vboxstock/raw/branch/main/public/assets/vboxstock-icon-512.png
title: title:
en_US: vBoxStock en_US: vBoxStock
tagline: tagline:
+2
View File
@@ -188,4 +188,6 @@ const targetedOcrObserver=new MutationObserver(()=>{const input=$("#receiveForm
const autoAddScanObserver=new MutationObserver(()=>{const form=$("#receiveForm"),help=form?.querySelector(".scan-help"),marker=form?.querySelector("[data-scan-added]");if(form&&marker&&!form.elements.uid.value&&!form.elements.sn.value&&!form.elements.mac.value)marker.remove();if(form&&help?.textContent.includes("read")&&form.elements.uid.value&&form.elements.sn.value&&form.elements.mac.value&&!marker){const added=document.createElement("i");added.dataset.scanAdded="true";added.hidden=true;form.append(added);form.querySelector(".add-device").click()}});autoAddScanObserver.observe($("#modalBody"),{childList:true,subtree:true,characterData:true}); const autoAddScanObserver=new MutationObserver(()=>{const form=$("#receiveForm"),help=form?.querySelector(".scan-help"),marker=form?.querySelector("[data-scan-added]");if(form&&marker&&!form.elements.uid.value&&!form.elements.sn.value&&!form.elements.mac.value)marker.remove();if(form&&help?.textContent.includes("read")&&form.elements.uid.value&&form.elements.sn.value&&form.elements.mac.value&&!marker){const added=document.createElement("i");added.dataset.scanAdded="true";added.hidden=true;form.append(added);form.querySelector(".add-device").click()}});autoAddScanObserver.observe($("#modalBody"),{childList:true,subtree:true,characterData:true});
if(!("BarcodeDetector" in window))document.addEventListener("click",e=>{const button=e.target.closest(".scan-camera");if(button){e.stopPropagation();scanLabelWithLibrary(button.closest("form"),button.closest("form").querySelector(".scan-help"))}},true); if(!("BarcodeDetector" in window))document.addEventListener("click",e=>{const button=e.target.closest(".scan-camera");if(button){e.stopPropagation();scanLabelWithLibrary(button.closest("form"),button.closest("form").querySelector(".scan-help"))}},true);
applyTheme(); applyTheme();
async function loadBuildInfo(){try{const response=await fetch("/api/health"),data=await response.json(),label=`v${data.appVersion}${data.buildSha&&data.buildSha!=="dev"?" · "+data.buildSha:""}`;document.querySelectorAll(".build-tag").forEach(el=>el.textContent=label)}catch{}}
loadBuildInfo();
initialize(); initialize();
+4 -4
View File
@@ -1,12 +1,12 @@
.customer-link{background:transparent!important;color:#1d4ed8!important;padding:0!important;text-align:left}.customer-link:hover{text-decoration:underline} .customer-link{background:transparent!important;color:#1d4ed8!important;padding:0!important;text-align:left}.customer-link:hover{text-decoration:underline}
#pagination{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;color:#68758b;font-size:13px;border-top:1px solid #edf0f5}#pagination div{display:flex;gap:8px}#pagination button{background:#fff;border:1px solid #d7deea;padding:7px 11px}#pagination button:disabled{opacity:.4;cursor:default} #pagination{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;color:#68758b;font-size:13px;border-top:1px solid #edf0f5}#pagination div{display:flex;gap:8px}#pagination button{height:var(--control-h-sm);padding:0 var(--control-px-sm);background:#fff;border:1px solid #d7deea}#pagination button:disabled{opacity:.4;cursor:default}
.address-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px}.detail-card{background:#f7f9fc;border:1px solid #e2e7ef;border-radius:10px;padding:14px;margin:14px 0}.detail-card h3,.customer-purchases h3{margin:0 0 10px}.detail-card p{margin:4px 0!important}.customer-purchases{max-height:330px;overflow:auto}.customer-purchases article{border-top:1px solid #e5e9f0;padding:12px 0}.customer-purchases article:first-child{border-top:0}.customer-purchases button{padding:6px 9px;background:#edf3ff;color:#1d4ed8}@media(max-width:760px){.address-grid{grid-template-columns:1fr}#pagination{align-items:flex-start;flex-direction:column}#pagination>div{width:100%;align-items:center;justify-content:space-between}} .address-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px}.detail-card{background:#f7f9fc;border:1px solid #e2e7ef;border-radius:10px;padding:14px;margin:14px 0}.detail-card h3,.customer-purchases h3{margin:0 0 10px}.detail-card p{margin:4px 0!important}.customer-purchases{max-height:330px;overflow:auto}.customer-purchases article{border-top:1px solid #e5e9f0;padding:12px 0}.customer-purchases article:first-child{border-top:0}.customer-purchases button{height:var(--control-h-sm);padding:0 var(--control-px-sm);background:#edf3ff;color:#1d4ed8}@media(max-width:760px){.address-grid{grid-template-columns:1fr}#pagination{align-items:flex-start;flex-direction:column}#pagination>div{width:100%;align-items:center;justify-content:space-between}}
.customer-notes{margin:18px 0}.customer-notes h3{margin-bottom:8px}.customer-notes article{position:relative;border:1px solid #e2e7ef;border-radius:9px;padding:12px;margin:8px 0}.customer-notes article>div{display:flex;align-items:center;gap:8px;color:#667085}.customer-notes article p{white-space:pre-wrap;margin:10px 0}.customer-notes article>button{position:absolute;right:8px;top:8px;padding:5px 8px;background:#fff0f0;color:#c62828}.note-category{background:#edf3ff;color:#1d4ed8;border-radius:99px;padding:3px 8px;font-size:12px;font-weight:700} .customer-notes{margin:18px 0}.customer-notes h3{margin-bottom:8px}.customer-notes article{position:relative;border:1px solid #e2e7ef;border-radius:9px;padding:12px;margin:8px 0}.customer-notes article>div{display:flex;align-items:center;gap:8px;color:#667085}.customer-notes article p{white-space:pre-wrap;margin:10px 0}.customer-notes article>button{position:absolute;right:8px;top:8px;padding:5px 8px;background:#fff0f0;color:#c62828}.note-category{background:#edf3ff;color:#1d4ed8;border-radius:99px;padding:3px 8px;font-size:12px;font-weight:700}
.storage-status{margin-left:auto!important;display:flex;align-items:center;gap:7px;white-space:nowrap}.storage-status i{width:9px;height:9px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22}.storage-status b{font-weight:600}.storage-status.connected i{background:#16a34a;box-shadow:0 0 0 3px #16a34a22}.storage-status.saving i,.storage-status.connecting i{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22;animation:status-pulse 1s infinite}.storage-status.critical{color:#b42318!important}.storage-status.critical i{background:#dc2626;box-shadow:0 0 0 3px #dc262622}@keyframes status-pulse{50%{opacity:.4}}@media(max-width:760px){.storage-status{display:flex!important;flex-basis:100%;margin-left:0!important;padding-bottom:10px}.records nav{height:auto;flex-wrap:wrap}} .storage-status{margin-left:auto!important;display:flex;align-items:center;gap:7px;white-space:nowrap}.storage-status i{width:9px;height:9px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22}.storage-status b{font-weight:600}.storage-status.connected i{background:#16a34a;box-shadow:0 0 0 3px #16a34a22}.storage-status.saving i,.storage-status.connecting i{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b22;animation:status-pulse 1s infinite}.storage-status.critical{color:#b42318!important}.storage-status.critical i{background:#dc2626;box-shadow:0 0 0 3px #dc262622}@keyframes status-pulse{50%{opacity:.4}}@media(max-width:760px){.storage-status{display:flex!important;flex-basis:100%;margin-left:0!important;padding-bottom:10px}.records nav{height:auto;flex-wrap:wrap}}
#adminPanel{padding:24px}.admin-page{display:grid;gap:20px}.admin-page h2{margin:0 0 5px}.admin-page p{color:#667085}.admin-actions{display:flex;gap:10px;flex-wrap:wrap}.upload-backup{display:inline-flex;margin:0;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;cursor:pointer}.upload-backup input{display:none}.backup-warning{padding:13px 15px;border-radius:9px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}.backup-list{border:1px solid #e3e7ef;border-radius:10px}.backup-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.backup-list article:last-child{border-bottom:0}.backup-list article small{display:block;color:#7b8799;margin-top:4px}.backup-list article>div:last-child{display:flex;gap:8px}.backup-list a.button{display:inline-flex;align-items:center;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;font-weight:650;text-decoration:none}.backup-list .delete-backup{background:#fff0f0;color:#c62828}.restart-message{text-align:center;padding:60px 20px}@media(max-width:760px){.backup-list article{align-items:flex-start;flex-direction:column}} #adminPanel{padding:24px}.admin-page{display:grid;gap:20px}.admin-page h2{margin:0 0 5px}.admin-page p{color:#667085}.admin-actions{display:flex;gap:10px;flex-wrap:wrap}.upload-backup{display:inline-flex;align-items:center;justify-content:center;margin:0;background:#fff;border:1px solid #d7deea;border-radius:var(--control-radius);height:var(--control-h);padding:0 var(--control-px);color:#27344a;cursor:pointer;box-sizing:border-box}.upload-backup input{display:none}.backup-warning{padding:13px 15px;border-radius:9px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}.backup-list{border:1px solid #e3e7ef;border-radius:10px}.backup-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.backup-list article:last-child{border-bottom:0}.backup-list article small{display:block;color:#7b8799;margin-top:4px}.backup-list article>div:last-child{display:flex;gap:8px}.backup-list a.button{display:inline-flex;align-items:center;justify-content:center;background:#fff;border:1px solid #d7deea;border-radius:var(--control-radius);height:var(--control-h);padding:0 var(--control-px);color:#27344a;font-weight:650;text-decoration:none;box-sizing:border-box}.backup-list .delete-backup{background:#fff0f0;color:#c62828}.restart-message{text-align:center;padding:60px 20px}@media(max-width:760px){.backup-list article{align-items:flex-start;flex-direction:column}}
.auth-pending #appShell,.auth-required #appShell{display:none}.role-admin #authScreen,.role-readonly #authScreen{display:none}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(145deg,#eef4ff,#f8fafc)}.auth-card{width:min(430px,100%);background:#fff;border:1px solid #dfe5ef;border-radius:18px;box-shadow:0 22px 60px #17203320;padding:34px}.auth-brand{margin-bottom:28px}.auth-card h1{margin:0 0 7px}.auth-card>div>p{color:#68758b}.auth-submit{width:100%;margin-top:14px}.auth-message{background:#fff0f0;color:#a51d1d;border:1px solid #fecaca;border-radius:8px;padding:10px 12px;margin:14px 0}.auth-logout{width:100%;margin-top:10px}.user-menu{display:flex;align-items:center;gap:10px;border-left:1px solid #e3e7ef;padding-left:18px}.user-menu span{display:grid;font-size:13px}.user-menu small{color:#758197}.admin-section{display:grid;gap:16px;border-bottom:1px solid #e3e7ef;padding-bottom:28px}.admin-section:last-child{border-bottom:0}.inline-user-form{display:grid;grid-template-columns:1fr 1fr 180px auto;gap:10px;align-items:end}.inline-user-form label{margin:0}.user-list,.audit-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.user-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.user-list article:last-child{border-bottom:0}.user-list small{display:block;color:#7b8799;margin-top:4px}.user-list article>div:last-child{display:flex;gap:7px;flex-wrap:wrap}.delete-backup{background:#fff0f0;color:#c62828}.audit-list{max-height:420px;overflow:auto}.audit-list article{display:grid;grid-template-columns:140px 180px 1fr;gap:12px;padding:10px 13px;border-bottom:1px solid #edf0f5;font-size:13px}.audit-list span{text-transform:capitalize}.audit-list small{color:#7b8799}@media(max-width:900px){.inline-user-form{grid-template-columns:1fr 1fr}.user-list article{align-items:flex-start;flex-direction:column}.audit-list article{grid-template-columns:1fr}.user-menu span{display:none}}@media(max-width:600px){.inline-user-form{grid-template-columns:1fr}.user-menu{padding-left:5px}.user-menu button{font-size:12px;padding:8px}.auth-card{padding:25px}} .auth-pending #appShell,.auth-required #appShell{display:none}.role-admin #authScreen,.role-readonly #authScreen{display:none}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(145deg,#eef4ff,#f8fafc)}.auth-card{width:min(430px,100%);background:#fff;border:1px solid #dfe5ef;border-radius:18px;box-shadow:0 22px 60px #17203320;padding:34px}.auth-brand{margin-bottom:28px}.auth-card h1{margin:0 0 7px}.auth-card>div>p{color:#68758b}.auth-submit{width:100%;margin-top:14px}.auth-message{background:#fff0f0;color:#a51d1d;border:1px solid #fecaca;border-radius:8px;padding:10px 12px;margin:14px 0}.auth-logout{width:100%;margin-top:10px}.user-menu{display:flex;align-items:center;gap:10px;border-left:1px solid #e3e7ef;padding-left:18px}.user-menu span{display:grid;font-size:13px}.user-menu small{color:#758197}.admin-section{display:grid;gap:16px;border-bottom:1px solid #e3e7ef;padding-bottom:28px}.admin-section:last-child{border-bottom:0}.inline-user-form{display:grid;grid-template-columns:1fr 1fr 180px auto;gap:10px;align-items:end}.inline-user-form label{margin:0}.user-list,.audit-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.user-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.user-list article:last-child{border-bottom:0}.user-list small{display:block;color:#7b8799;margin-top:4px}.user-list article>div:last-child{display:flex;gap:7px;flex-wrap:wrap}.delete-backup{background:#fff0f0;color:#c62828}.audit-list{max-height:420px;overflow:auto}.audit-list article{display:grid;grid-template-columns:140px 180px 1fr;gap:12px;padding:10px 13px;border-bottom:1px solid #edf0f5;font-size:13px}.audit-list span{text-transform:capitalize}.audit-list small{color:#7b8799}@media(max-width:900px){.inline-user-form{grid-template-columns:1fr 1fr}.user-list article{align-items:flex-start;flex-direction:column}.audit-list article{grid-template-columns:1fr}.user-menu span{display:none}}@media(max-width:600px){.inline-user-form{grid-template-columns:1fr}.user-menu{padding-left:5px}.user-menu button{font-size:12px;padding:8px}.auth-card{padding:25px}}
.inline-model-form{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:10px;align-items:end}.inline-model-form label{margin:0}.model-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.model-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.model-list article:last-child{border-bottom:0}.model-summary>div{display:flex;align-items:center;gap:9px}.model-summary small{display:block;color:#7b8799;margin-top:5px}.model-status{display:inline-flex;border-radius:99px;padding:3px 8px;font-size:11px;font-weight:750}.model-status.active{background:#dcfce7;color:#166534}.model-status.archived{background:#eef1f5;color:#526071}.model-actions{display:flex;gap:7px;flex-wrap:wrap}@media(max-width:760px){.inline-model-form{grid-template-columns:1fr}.model-list article{align-items:stretch;flex-direction:column}.model-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.model-actions button{text-align:center}} .inline-model-form{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:10px;align-items:end}.inline-model-form label{margin:0}.model-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.model-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.model-list article:last-child{border-bottom:0}.model-summary>div{display:flex;align-items:center;gap:9px}.model-summary small{display:block;color:#7b8799;margin-top:5px}.model-status{display:inline-flex;border-radius:99px;padding:3px 8px;font-size:11px;font-weight:750}.model-status.active{background:#dcfce7;color:#166534}.model-status.archived{background:#eef1f5;color:#526071}.model-actions{display:flex;gap:7px;flex-wrap:wrap}@media(max-width:760px){.inline-model-form{grid-template-columns:1fr}.model-list article{align-items:stretch;flex-direction:column}.model-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.model-actions button{text-align:center}}
.conditional-fields{margin:2px 0 14px;padding:16px;border:1px solid #e3e7ef;border-radius:11px;background:#f8fafc}.conditional-fields h3{margin:0 0 12px}.conditional-hint{margin:4px 0 16px;padding:13px;border:1px dashed #d7deea;border-radius:9px;color:#68758b}.field-help{display:block;margin-top:-8px;color:#68758b}.warranty-pill{display:inline-flex;align-items:center;border-radius:99px;padding:5px 9px;font-size:12px;font-weight:750;white-space:nowrap}.warranty-pill.in-warranty{background:#dcfce7;color:#166534}.warranty-pill.expired{background:#fee2e2;color:#b91c1c}.warranty-pill.neutral{background:#eef1f5;color:#526071}.inline-warranty-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px 150px auto;gap:10px;align-items:end}.inline-warranty-form label{margin:0}.warranty-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.warranty-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.warranty-list article:last-child{border-bottom:0}@media(max-width:760px){.inline-warranty-form{grid-template-columns:1fr}.warranty-list article{align-items:stretch;flex-direction:column}} .conditional-fields{margin:2px 0 14px;padding:16px;border:1px solid #e3e7ef;border-radius:11px;background:#f8fafc}.conditional-fields h3{margin:0 0 12px}.conditional-hint{margin:4px 0 16px;padding:13px;border:1px dashed #d7deea;border-radius:9px;color:#68758b}.field-help{display:block;margin-top:-8px;color:#68758b}.warranty-pill{display:inline-flex;align-items:center;border-radius:99px;padding:5px 9px;font-size:12px;font-weight:750;white-space:nowrap}.warranty-pill.in-warranty{background:#dcfce7;color:#166534}.warranty-pill.expired{background:#fee2e2;color:#b91c1c}.warranty-pill.neutral{background:#eef1f5;color:#526071}.inline-warranty-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px 150px auto;gap:10px;align-items:end}.inline-warranty-form label{margin:0}.warranty-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.warranty-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.warranty-list article:last-child{border-bottom:0}@media(max-width:760px){.inline-warranty-form{grid-template-columns:1fr}.warranty-list article{align-items:stretch;flex-direction:column}}
.sale-items{margin:18px 0;padding:0;border:1px solid #d7deea;border-radius:11px;overflow:hidden}.sale-items legend{margin-left:12px;padding:0 6px;font-size:13px;font-weight:750}.sale-item-list{max-height:250px;overflow:auto}.sale-item{display:grid;grid-template-columns:auto minmax(0,1fr) 130px;align-items:center;gap:11px;margin:0;padding:11px 13px;border-bottom:1px solid #edf0f5}.sale-item>input{width:18px;height:18px}.sale-item small{display:block;margin-top:3px;color:#68758b}.sale-item-price{font-size:12px}.sale-item-price input{margin-top:4px;text-align:right}.sale-summary{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:#f8fafc}.sale-summary span{color:#68758b}@media(max-width:520px){.sale-item{grid-template-columns:auto minmax(0,1fr)}.sale-item-price{grid-column:2}.sale-item-list{max-height:310px}} .sale-items{margin:18px 0;padding:0;border:1px solid #d7deea;border-radius:11px;overflow:hidden}.sale-items legend{margin-left:12px;padding:0 6px;font-size:13px;font-weight:750}.sale-item-list{max-height:250px;overflow:auto}.sale-item{display:grid;grid-template-columns:auto minmax(0,1fr) 130px;align-items:center;gap:11px;margin:0;padding:11px 13px;border-bottom:1px solid #edf0f5}.sale-item>input{width:18px;height:18px}.sale-item small{display:block;margin-top:3px;color:#68758b}.sale-item-price{font-size:12px}.sale-item-price input{margin-top:4px;text-align:right}.sale-summary{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:#f8fafc}.sale-summary span{color:#68758b}@media(max-width:520px){.sale-item{grid-template-columns:auto minmax(0,1fr)}.sale-item-price{grid-column:2}.sale-item-list{max-height:310px}}
.scan-panel{margin:16px 0;padding:14px;border:1px solid #d7deea;border-radius:11px;background:#f8fafc}.scan-panel-heading{display:flex;justify-content:space-between;align-items:center;gap:10px}.scan-panel-heading h3{margin:0}.scan-panel-heading span{font-size:12px;color:#68758b}.scan-panel .scan-camera{margin-right:8px}.batch-list{display:grid;gap:6px;margin:12px 0}.batch-row{display:grid;grid-template-columns:auto repeat(3,minmax(0,1fr)) auto;align-items:center;gap:8px;padding:9px;border:1px solid #e3e7ef;border-radius:8px;font-size:12px}.empty-batch{margin:0;padding:12px;border:1px dashed #d7deea;border-radius:8px;color:#68758b;text-align:center}.scanner-overlay{position:fixed;inset:0;z-index:20;display:grid;place-items:center;padding:20px;background:#000c}.scanner-overlay>video{width:min(94vw,620px);max-height:78vh;object-fit:contain;border-radius:14px}.scanner-overlay>div{position:absolute;top:20px;left:20px;right:20px;display:flex;align-items:center;gap:12px;padding:12px 14px;background:#172033eF;color:#fff;border-radius:10px}.scanner-overlay small{display:block;opacity:.8}.scanner-overlay button{margin-left:auto;background:#fff;color:#172033}@media(max-width:760px){.batch-row{grid-template-columns:auto 1fr auto}.batch-row span{grid-column:2}.scan-panel .scan-camera{margin:0 0 8px;width:100%}.scan-panel .add-device{width:100%}} .scan-panel{margin:16px 0;padding:14px;border:1px solid #d7deea;border-radius:11px;background:#f8fafc}.scan-panel-heading{display:flex;justify-content:space-between;align-items:center;gap:10px}.scan-panel-heading h3{margin:0}.scan-panel-heading span{font-size:12px;color:#68758b}.scan-panel .scan-camera{margin-right:8px}.batch-list{display:grid;gap:6px;margin:12px 0}.batch-row{display:grid;grid-template-columns:auto repeat(3,minmax(0,1fr)) auto;align-items:center;gap:8px;padding:9px;border:1px solid #e3e7ef;border-radius:8px;font-size:12px}.empty-batch{margin:0;padding:12px;border:1px dashed #d7deea;border-radius:8px;color:#68758b;text-align:center}.scanner-overlay{position:fixed;inset:0;z-index:20;display:grid;place-items:center;padding:20px;background:#000c}.scanner-overlay>video{width:min(94vw,620px);max-height:78vh;object-fit:contain;border-radius:14px}.scanner-overlay>div{position:absolute;top:20px;left:20px;right:20px;display:flex;align-items:center;gap:12px;padding:12px 14px;background:#172033eF;color:#fff;border-radius:10px}.scanner-overlay small{display:block;opacity:.8}.scanner-overlay button{margin-left:auto;background:#fff;color:#172033}@media(max-width:760px){.batch-row{grid-template-columns:auto 1fr auto}.batch-row span{grid-column:2}.scan-panel .scan-camera{margin:0 0 8px;width:100%}.scan-panel .add-device{width:100%}}
.filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{padding:8px 9px;font-size:13px}.filter-bar .button,.filter-bar button{padding:9px 12px}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.automatic-backups{display:grid;gap:12px;padding:16px;border:1px solid #e3e7ef;border-radius:10px}.automatic-backups h3,.automatic-backups p{margin:0}.backup-schedule{display:grid;grid-template-columns:minmax(190px,1.3fr) repeat(4,minmax(130px,1fr)) auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.weekly-field[hidden]{display:none!important}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.backup-schedule-status{display:flex;gap:18px;flex-wrap:wrap;font-size:13px}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:1100px){.backup-schedule{grid-template-columns:1fr 1fr 1fr}}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}} .filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{height:var(--control-h-sm);padding:0 var(--control-px-sm);font-size:var(--control-font-sm);box-sizing:border-box}.filter-bar .button,.filter-bar button{height:var(--control-h-sm);padding:0 var(--control-px-sm);display:inline-flex;align-items:center;justify-content:center}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.automatic-backups{display:grid;gap:12px;padding:16px;border:1px solid #e3e7ef;border-radius:10px}.automatic-backups h3,.automatic-backups p{margin:0}.backup-schedule{display:grid;grid-template-columns:minmax(190px,1.3fr) repeat(4,minmax(130px,1fr)) auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.weekly-field[hidden]{display:none!important}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.backup-schedule-status{display:flex;gap:18px;flex-wrap:wrap;font-size:13px}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:1100px){.backup-schedule{grid-template-columns:1fr 1fr 1fr}}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}}
+1 -1
View File
@@ -1,5 +1,5 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f5f7fb"><meta name="application-name" content="vBoxStock"><meta name="apple-mobile-web-app-title" content="vBoxStock"><title>vBoxStock</title><script>try{const t=localStorage.getItem("vboxstock-theme")||"system",d=t==="dark"||(t==="system"&&matchMedia("(prefers-color-scheme:dark)").matches);document.documentElement.dataset.theme=d?"dark":"light"}catch{}</script><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="manifest" href="/site.webmanifest"><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/extras.css"><link rel="stylesheet" href="/theme.css"></head> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f5f7fb"><meta name="application-name" content="vBoxStock"><meta name="apple-mobile-web-app-title" content="vBoxStock"><title>vBoxStock</title><script>try{const t=localStorage.getItem("vboxstock-theme")||"system",d=t==="dark"||(t==="system"&&matchMedia("(prefers-color-scheme:dark)").matches);document.documentElement.dataset.theme=d?"dark":"light"}catch{}</script><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="manifest" href="/site.webmanifest"><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/extras.css"><link rel="stylesheet" href="/theme.css"></head>
<body class="auth-pending"><section id="authScreen" class="auth-screen"><div class="auth-card"><div class="brand auth-brand"><img src="/assets/vboxstock-icon-512.png" alt=""><span>vBoxStock</span></div><div id="authBody"></div></div></section><div id="appShell"><header><div class="brand"><img src="/assets/vboxstock-icon-512.png" alt=""><span>vBoxStock</span></div><input id="search" type="search" aria-label="Search inventory, sales, and customers" placeholder="Search UID, SN, MAC, model, customer…"><div class="actions edit-only"><button class="secondary" data-open="receive"> Receive product</button><button class="primary" data-open="sell">Record a sale</button></div><div class="user-menu"><span id="currentUser"></span><label class="theme-control"><span>Theme</span><select id="themeSelect" aria-label="Color theme"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label><button class="secondary" id="logout">Log out</button></div></header> <body class="auth-pending"><section id="authScreen" class="auth-screen"><div class="auth-card"><div class="brand auth-brand"><img src="/assets/vboxstock-icon-512.png" alt=""><span>vBoxStock</span></div><small class="build-tag" id="buildTagAuth"></small><div id="authBody"></div></div></section><div id="appShell"><header><div class="header-inner"><div class="brand"><img src="/assets/vboxstock-icon-512.png" alt=""><span>vBoxStock</span></div><input id="search" type="search" aria-label="Search inventory, sales, and customers" placeholder="Search UID, SN, MAC, model, customer…"><div class="actions edit-only"><button class="secondary" data-open="receive"> Receive product</button><button class="primary" data-open="sell">Record a sale</button></div><div class="user-menu"><small class="build-tag" id="buildTagHeader"></small><span id="currentUser"></span><label class="theme-control"><span>Theme</span><select id="themeSelect" aria-label="Color theme"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label><button class="secondary" id="logout">Log out</button></div></div></header>
<main><div class="intro"><h1>Inventory Overview</h1><span id="date"></span></div><section class="stats"><article><span>Available products</span><strong id="availableCount">0</strong><small>Ready to sell</small></article><article><span>Sold this month</span><strong id="soldCount">0</strong><small id="revenue">No sales recorded</small></article><article><span>Inventory value</span><strong id="value">$0</strong><small>Based on purchase cost</small></article></section> <main><div class="intro"><h1>Inventory Overview</h1><span id="date"></span></div><section class="stats"><article><span>Available products</span><strong id="availableCount">0</strong><small>Ready to sell</small></article><article><span>Sold this month</span><strong id="soldCount">0</strong><small id="revenue">No sales recorded</small></article><article><span>Inventory value</span><strong id="value">$0</strong><small>Based on purchase cost</small></article></section>
<section class="records"><nav><button class="tab active" data-tab="available">Available inventory <i id="availableBadge">0</i></button><button class="tab" data-tab="sold">Sales history <i id="soldBadge">0</i></button><button class="tab" data-tab="customers">Customers <i id="customerBadge">0</i></button><button class="tab" data-tab="admin">Admin</button><span id="storageStatus" class="storage-status connecting" role="status" aria-live="polite"><i></i><b>Connecting to database</b></span></nav><div id="filterBar" class="filter-bar"></div><div class="table-wrap"><table><thead id="thead"></thead><tbody id="rows"></tbody></table><div id="empty" hidden></div></div><div id="adminPanel" hidden></div><footer id="pagination"></footer></section></main> <section class="records"><nav><button class="tab active" data-tab="available">Available inventory <i id="availableBadge">0</i></button><button class="tab" data-tab="sold">Sales history <i id="soldBadge">0</i></button><button class="tab" data-tab="customers">Customers <i id="customerBadge">0</i></button><button class="tab" data-tab="admin">Admin</button><span id="storageStatus" class="storage-status connecting" role="status" aria-live="polite"><i></i><b>Connecting to database</b></span></nav><div id="filterBar" class="filter-bar"></div><div class="table-wrap"><table><thead id="thead"></thead><tbody id="rows"></tbody></table><div id="empty" hidden></div></div><div id="adminPanel" hidden></div><footer id="pagination"></footer></section></main>
<dialog id="modal"><button class="close" aria-label="Close">×</button><div id="modalBody"></div></dialog><div id="toast" hidden></div></div><script type="module" src="/app.js"></script></body></html> <dialog id="modal"><button class="close" aria-label="Close">×</button><div id="modalBody"></div></dialog><div id="toast" hidden></div></div><script type="module" src="/app.js"></script></body></html>
+2 -1
View File
@@ -1 +1,2 @@
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#172033;background:#f5f7fb}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{cursor:pointer}header{height:76px;background:white;border-bottom:1px solid #e3e7ef;display:flex;align-items:center;padding:0 4vw;gap:28px;position:sticky;top:0;z-index:2}.brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:700}.brand b{display:grid;place-items:center;background:#2563eb;color:white;border-radius:9px;width:36px;height:36px}.brand+input{flex:1;max-width:620px;border:1px solid #d8deea;background:#f8fafc;padding:12px 15px;border-radius:10px}.actions{margin-left:auto;display:flex;gap:10px}button{border:0;border-radius:9px;padding:10px 14px;font-weight:650}.primary{background:#2563eb;color:#fff}.secondary{background:#fff;border:1px solid #d7deea;color:#27344a}main{width:min(1400px,92vw);margin:34px auto}.intro{display:flex;justify-content:space-between;align-items:center}.intro h1{font-size:28px}.intro>span{background:#fff;border:1px solid #e0e5ed;padding:9px 12px;border-radius:9px;color:#64748b}.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin:20px 0}.stats article{background:#fff;border:1px solid #e3e7ef;border-radius:14px;padding:22px;display:grid;gap:6px}.stats span,.stats small{color:#68758b}.stats strong{font-size:30px}.records{background:#fff;border:1px solid #e3e7ef;border-radius:14px;overflow:hidden}.records nav{height:64px;border-bottom:1px solid #e3e7ef;display:flex;align-items:center;padding:0 20px;gap:12px}.records nav .tab{background:transparent;color:#667085;border-radius:0;height:64px}.records nav .tab.active{color:#2563eb;border-bottom:2px solid #2563eb}.tab i{font-style:normal;background:#edf2f8;padding:2px 7px;border-radius:12px}.records nav>span{margin-left:auto;color:#6b7280;font-size:13px}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;min-width:950px}th,td{text-align:left;padding:15px 18px;border-bottom:1px solid #edf0f5;vertical-align:middle}th{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#758197;background:#fbfcfe}.product strong,.customer strong{display:block}.muted,td small{display:block;color:#7b8799;margin-top:4px}.ids{display:grid;gap:3px}.ids code{font-family:ui-monospace,monospace;font-size:12px}.pill{display:inline-block;color:#087443;background:#dcfce7;border-radius:99px;padding:4px 9px;font-size:12px}.row-actions{display:flex;gap:6px}.row-actions button{padding:7px 9px;background:#edf3ff;color:#1d4ed8;white-space:nowrap}.row-actions .danger{background:#fff0f0;color:#c62828}#empty{padding:60px;text-align:center;color:#758197}dialog{width:min(620px,94vw);border:0;border-radius:16px;padding:26px;box-shadow:0 24px 80px #17203340}dialog::backdrop{background:#17203380}.close{position:absolute;right:15px;top:13px;background:transparent;font-size:25px;color:#728096}dialog h2{margin:4px 0}dialog>div>p{color:#68758b;margin-top:5px}.scan{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}label{display:grid;gap:6px;margin:13px 0;font-weight:600;font-size:13px}label input,label select,label textarea{width:100%;padding:11px;border:1px solid #d6dce7;border-radius:8px;background:white}.form-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:20px}#toast{position:fixed;right:24px;bottom:24px;background:#172033;color:#fff;padding:13px 17px;border-radius:10px;z-index:4}@media(max-width:760px){header{height:auto;padding:14px;flex-wrap:wrap;gap:10px}.brand+input{order:3;flex-basis:100%;max-width:none}.actions{margin-left:auto}.actions button{font-size:0}.actions button::first-letter{font-size:18px}.stats{grid-template-columns:1fr}.scan,.form-row{grid-template-columns:1fr}.records nav>span{display:none}main{margin-top:18px}} :root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#172033;background:#f5f7fb;--control-h:44px;--control-h-sm:36px;--control-radius:9px;--control-radius-sm:8px;--control-px:14px;--control-px-sm:10px;--control-font:14px;--control-font-sm:13px}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{cursor:pointer}header{height:76px;background:white;border-bottom:1px solid #e3e7ef;position:sticky;top:0;z-index:2}.header-inner{width:min(1400px,92vw);margin:0 auto;height:100%;display:flex;align-items:center;gap:28px}.brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:700}.brand b{display:grid;place-items:center;background:#2563eb;color:white;border-radius:9px;width:36px;height:36px}.brand+input{flex:1;max-width:620px;border:1px solid #d8deea;background:#f8fafc;height:var(--control-h);padding:0 var(--control-px);border-radius:var(--control-radius);box-sizing:border-box;font-size:var(--control-font)}.actions{margin-left:auto;display:flex;gap:10px}button{border:0;border-radius:var(--control-radius);height:var(--control-h);padding:0 var(--control-px);font-weight:650;font-size:var(--control-font);display:inline-flex;align-items:center;justify-content:center}.primary{background:#2563eb;color:#fff}.secondary{background:#fff;border:1px solid #d7deea;color:#27344a}main{width:min(1400px,92vw);margin:34px auto}.intro{display:flex;justify-content:space-between;align-items:center}.intro h1{font-size:28px}.intro>span{background:#fff;border:1px solid #e0e5ed;padding:9px 12px;border-radius:9px;color:#64748b}.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin:20px 0}.stats article{background:#fff;border:1px solid #e3e7ef;border-radius:14px;padding:22px;display:grid;gap:6px}.stats span,.stats small{color:#68758b}.stats strong{font-size:30px}.records{background:#fff;border:1px solid #e3e7ef;border-radius:14px;overflow:hidden}.records nav{height:64px;border-bottom:1px solid #e3e7ef;display:flex;align-items:center;padding:0 20px;gap:12px}.records nav .tab{background:transparent;color:#667085;border-radius:0;height:64px}.records nav .tab.active{color:#2563eb;border-bottom:2px solid #2563eb}.tab i{font-style:normal;background:#edf2f8;padding:2px 7px;border-radius:12px}.records nav>span{margin-left:auto;color:#6b7280;font-size:13px}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;min-width:950px}th,td{text-align:left;padding:15px 18px;border-bottom:1px solid #edf0f5;vertical-align:middle}th{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#758197;background:#fbfcfe}.product strong,.customer strong{display:block}.muted,td small{display:block;color:#7b8799;margin-top:4px}.ids{display:grid;gap:3px}.ids code{font-family:ui-monospace,monospace;font-size:12px}.pill{display:inline-block;color:#087443;background:#dcfce7;border-radius:99px;padding:4px 9px;font-size:12px}.row-actions{display:flex;gap:6px}.row-actions button{height:var(--control-h-sm);padding:0 var(--control-px-sm);background:#edf3ff;color:#1d4ed8;white-space:nowrap;font-size:var(--control-font-sm)}.row-actions .danger{background:#fff0f0;color:#c62828}#empty{padding:60px;text-align:center;color:#758197}dialog{width:min(620px,94vw);border:0;border-radius:16px;padding:26px;box-shadow:0 24px 80px #17203340}dialog::backdrop{background:#17203380}.close{position:absolute;right:15px;top:13px;background:transparent;font-size:25px;color:#728096}dialog h2{margin:4px 0}dialog>div>p{color:#68758b;margin-top:5px}.scan{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}label{display:grid;gap:6px;margin:13px 0;font-weight:600;font-size:13px}label input,label select,label textarea{width:100%;height:var(--control-h);padding:0 var(--control-px);border:1px solid #d6dce7;border-radius:var(--control-radius);background:white;box-sizing:border-box;font-size:var(--control-font)}label textarea{height:auto;min-height:var(--control-h);padding:11px var(--control-px)}.form-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:20px}#toast{position:fixed;right:24px;bottom:24px;background:#172033;color:#fff;padding:13px 17px;border-radius:10px;z-index:4}@media(max-width:760px){header{height:auto}.header-inner{width:100%;padding:14px;flex-wrap:wrap;gap:10px;box-sizing:border-box}.brand+input{order:3;flex-basis:100%;max-width:none}.actions{margin-left:auto}.actions button{font-size:0}.actions button::first-letter{font-size:18px}.stats{grid-template-columns:1fr}.scan,.form-row{grid-template-columns:1fr}.records nav>span{display:none}main{margin-top:18px}}
.build-tag{color:#9aa5b8;font-size:11px;font-weight:600;letter-spacing:.02em}
+5 -4
View File
@@ -32,7 +32,7 @@ th{background:var(--surface-table);color:var(--muted-2)}th,td,.records nav,.back
.auth-screen{background:linear-gradient(145deg,color-mix(in srgb,var(--blue) 10%,var(--page)),var(--page))}.auth-card{box-shadow:0 22px 60px var(--shadow)} .auth-screen{background:linear-gradient(145deg,color-mix(in srgb,var(--blue) 10%,var(--page)),var(--page))}.auth-card{box-shadow:0 22px 60px var(--shadow)}
dialog{max-height:min(90dvh,900px);overflow:auto}dialog::backdrop{background:#050914aa}.close{color:var(--muted)} dialog{max-height:min(90dvh,900px);overflow:auto}dialog::backdrop{background:#050914aa}.close{color:var(--muted)}
#toast{background:var(--text-strong);color:var(--surface);box-shadow:0 12px 30px var(--shadow)} #toast{background:var(--text-strong);color:var(--surface);box-shadow:0 12px 30px var(--shadow)}
#currentUser{background:var(--surface-soft);border:1px solid var(--border-strong);border-radius:9px;padding:6px 10px;line-height:1.15}.theme-control{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px;margin:0 0 0 4px;padding-left:12px;border-left:1px solid var(--border-strong);color:var(--muted);font-size:11px}.user-menu select{width:92px;border:1px solid var(--border-strong);border-radius:9px;padding:9px 26px 9px 9px;font-size:12px;font-weight:650} #currentUser{background:var(--surface-soft);border:1px solid var(--border-strong);border-radius:9px;padding:6px 10px;line-height:1.15}.theme-control{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px;margin:0 0 0 4px;padding-left:12px;border-left:1px solid var(--border-strong);color:var(--muted);font-size:11px}.user-menu select{width:92px;height:var(--control-h);box-sizing:border-box;border:1px solid var(--border-strong);border-radius:var(--control-radius);padding:0 26px 0 9px;font-size:12px;font-weight:650}
#pagination button{background:var(--surface-soft);border-color:var(--border-strong);color:var(--text)}#pagination button:not(:disabled):hover{background:var(--blue-soft);color:var(--blue-text)}#pagination button:disabled{background:var(--surface-soft);color:var(--text);opacity:1;cursor:not-allowed} #pagination button{background:var(--surface-soft);border-color:var(--border-strong);color:var(--text)}#pagination button:not(:disabled):hover{background:var(--blue-soft);color:var(--blue-text)}#pagination button:disabled{background:var(--surface-soft);color:var(--text);opacity:1;cursor:not-allowed}
button,.button,input,select,textarea{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid color-mix(in srgb,var(--blue) 45%,transparent);outline-offset:2px} button,.button,input,select,textarea{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid color-mix(in srgb,var(--blue) 45%,transparent);outline-offset:2px}
.brand{font-weight:750;white-space:nowrap}.brand img{width:42px;height:42px;object-fit:contain;filter:drop-shadow(0 2px 4px var(--shadow))}.auth-brand img{width:62px;height:62px}.admin-intro{display:flex;align-items:center;gap:14px;padding:4px 0 2px}.admin-intro img{width:54px;height:54px;object-fit:contain}.admin-intro h2{margin:0}.admin-intro p{margin:4px 0 0}.admin-kicker{display:block;color:var(--blue-text);font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase} .brand{font-weight:750;white-space:nowrap}.brand img{width:42px;height:42px;object-fit:contain;filter:drop-shadow(0 2px 4px var(--shadow))}.auth-brand img{width:62px;height:62px}.admin-intro{display:flex;align-items:center;gap:14px;padding:4px 0 2px}.admin-intro img{width:54px;height:54px;object-fit:contain}.admin-intro h2{margin:0}.admin-intro p{margin:4px 0 0}.admin-kicker{display:block;color:var(--blue-text);font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
@@ -44,9 +44,10 @@ button,.button,input,select,textarea{-webkit-tap-highlight-color:transparent}but
} }
@media (max-width:820px){ @media (max-width:820px){
html,body{max-width:100%;overflow-x:hidden}input,select,textarea{font-size:16px!important}.auth-screen{min-height:100dvh;width:100%;overflow-x:hidden}.auth-card{max-width:100%} html,body{max-width:100%;overflow-x:hidden}input,select,textarea{font-size:16px!important}.auth-screen{min-height:100dvh;width:100%;overflow-x:hidden}.auth-card{max-width:100%}
header{position:relative;display:grid;grid-template-columns:auto 1fr auto;height:auto;padding:12px max(14px,env(safe-area-inset-right)) 12px max(14px,env(safe-area-inset-left));gap:10px} header{position:relative;height:auto}
header .brand{grid-column:1}.user-menu{grid-column:2/4;grid-row:1;margin-left:auto!important;min-width:0!important;padding-left:0;border-left:0}.user-menu #currentUser{display:grid!important;width:auto!important;min-width:78px;padding:5px 7px}.theme-control{grid-template-columns:1fr;margin-left:0;padding-left:6px}.theme-control>span{font-size:10px;text-align:center} .header-inner{display:grid;grid-template-columns:auto 1fr auto;width:100%;box-sizing:border-box;padding:12px max(14px,env(safe-area-inset-right)) 12px max(14px,env(safe-area-inset-left));gap:10px}
header #search{grid-column:1/-1;grid-row:2;order:initial}.actions{grid-column:1/-1;grid-row:3;width:100%;margin:0}.actions button{font-size:14px!important;flex:1}.actions button::first-letter{font-size:inherit!important} .header-inner .brand{grid-column:1}.user-menu{grid-column:2/4;grid-row:1;margin-left:auto!important;min-width:0!important;padding-left:0;border-left:0}.user-menu #currentUser{display:grid!important;width:auto!important;min-width:78px;padding:5px 7px}.theme-control{grid-template-columns:1fr;margin-left:0;padding-left:6px}.theme-control>span{font-size:10px;text-align:center}
.header-inner #search{grid-column:1/-1;grid-row:2;order:initial}.actions{grid-column:1/-1;grid-row:3;width:100%;margin:0}.actions button{font-size:14px!important;flex:1}.actions button::first-letter{font-size:inherit!important}
main{width:min(100% - 24px,1400px);margin:14px auto 28px}.intro h1{font-size:23px}.intro>span{font-size:12px}.stats{gap:10px;margin:14px 0}.stats article{padding:16px}.stats strong{font-size:26px} main{width:min(100% - 24px,1400px);margin:14px auto 28px}.intro h1{font-size:23px}.intro>span{font-size:12px}.stats{gap:10px;margin:14px 0}.stats article{padding:16px}.stats strong{font-size:26px}
.records{overflow:visible}.records nav{display:grid;grid-template-columns:1fr 1fr;overflow:visible;height:auto;padding:0 10px;gap:0}.records nav .tab{width:100%;height:50px;white-space:nowrap;padding-inline:6px;font-size:12px}.storage-status{grid-column:1/-1;min-width:100%;margin:0!important;padding:10px 4px!important;border-top:1px solid var(--border)} .records{overflow:visible}.records nav{display:grid;grid-template-columns:1fr 1fr;overflow:visible;height:auto;padding:0 10px;gap:0}.records nav .tab{width:100%;height:50px;white-space:nowrap;padding-inline:6px;font-size:12px}.storage-status{grid-column:1/-1;min-width:100%;margin:0!important;padding:10px 4px!important;border-top:1px solid var(--border)}
.table-wrap{overflow:visible}table{display:block;min-width:0}thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}tbody{display:grid;gap:10px;padding:10px;background:var(--page)}tr{display:grid;background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}td{display:grid;grid-template-columns:minmax(92px,34%) 1fr;gap:10px;align-items:start;padding:11px 13px;border-bottom:1px solid var(--border);overflow-wrap:anywhere}td:last-child{border-bottom:0}td::before{content:attr(data-label);font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.04em;color:var(--muted-2)}td:empty{display:none}.row-actions{flex-wrap:wrap}.row-actions button{flex:1;min-width:95px} .table-wrap{overflow:visible}table{display:block;min-width:0}thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}tbody{display:grid;gap:10px;padding:10px;background:var(--page)}tr{display:grid;background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}td{display:grid;grid-template-columns:minmax(92px,34%) 1fr;gap:10px;align-items:start;padding:11px 13px;border-bottom:1px solid var(--border);overflow-wrap:anywhere}td:last-child{border-bottom:0}td::before{content:attr(data-label);font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.04em;color:var(--muted-2)}td:empty{display:none}.row-actions{flex-wrap:wrap}.row-actions button{flex:1;min-width:95px}
+3 -3
View File
@@ -5,7 +5,7 @@ import { extname, join, normalize } from "node:path";
import { backup, DatabaseSync } from "node:sqlite"; import { backup, DatabaseSync } from "node:sqlite";
import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto"; import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
const APP_VERSION="3.5.0",SCHEMA_VERSION=3,port=Number(process.env.PORT||3000),dataDir=process.env.DATA_DIR||"/data"; const APP_VERSION="3.5.0",SCHEMA_VERSION=3,port=Number(process.env.PORT||3000),dataDir=process.env.DATA_DIR||"/data",GIT_SHA=process.env.GIT_SHA||"dev",BUILD_SHORT_SHA=GIT_SHA==="dev"?"dev":GIT_SHA.slice(0,7),BUILD_DATE=process.env.BUILD_DATE||"";
const databasePath=join(dataDir,"vboxstock.db"),backupDir=join(dataDir,"backups"),restoreMarker=join(dataDir,".restore-audit.json"),publicDir=join(import.meta.dirname,"public"); const databasePath=join(dataDir,"vboxstock.db"),backupDir=join(dataDir,"backups"),restoreMarker=join(dataDir,".restore-audit.json"),publicDir=join(import.meta.dirname,"public");
const SESSION_IDLE_MS=12*60*60*1000,sessions=new Map(),loginFailures=new Map(); const SESSION_IDLE_MS=12*60*60*1000,sessions=new Map(),loginFailures=new Map();
mkdirSync(dataDir,{recursive:true});mkdirSync(backupDir,{recursive:true}); mkdirSync(dataDir,{recursive:true});mkdirSync(backupDir,{recursive:true});
@@ -123,7 +123,7 @@ async function authApi(req,res,url,session){
async function adminApi(req,res,url,session){ async function adminApi(req,res,url,session){
const user=session.user; const user=session.user;
if(url.pathname==="/api/admin/diagnostics"&&req.method==="GET"){const databaseInfo=await stat(databasePath),disk=await statfs(dataDir),backups=await backupFiles();let writable=true;try{const probe=join(dataDir,`.write-test-${crypto.randomUUID()}`);await writeFile(probe,"ok");await unlink(probe)}catch{writable=false}return json(res,200,{appVersion:APP_VERSION,schemaVersion:Number(setting("schema_version"))||SCHEMA_VERSION,nodeVersion:process.version,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,dataDirectory:dataDir,dataWritable:writable,databaseSize:databaseInfo.size,backupCount:backups.length,lastBackupAt:backups[0]?.createdAt||null,diskFree:disk.bavail*disk.bsize,diskTotal:disk.blocks*disk.bsize,backupSettings:{enabled:setting("backup_enabled")==="1",frequency:setting("backup_frequency")==="weekly"?"weekly":"daily",weekday:Number(setting("backup_weekday"))||0,hour:Number(setting("backup_hour"))||0,retention:Number(setting("backup_retention"))||14,lastScheduledBackup:setting("last_scheduled_backup")||null,nextScheduledBackup:nextAutomaticBackup()}})} if(url.pathname==="/api/admin/diagnostics"&&req.method==="GET"){const databaseInfo=await stat(databasePath),disk=await statfs(dataDir),backups=await backupFiles();let writable=true;try{const probe=join(dataDir,`.write-test-${crypto.randomUUID()}`);await writeFile(probe,"ok");await unlink(probe)}catch{writable=false}return json(res,200,{appVersion:APP_VERSION,buildSha:BUILD_SHORT_SHA,buildDate:BUILD_DATE,schemaVersion:Number(setting("schema_version"))||SCHEMA_VERSION,nodeVersion:process.version,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,dataDirectory:dataDir,dataWritable:writable,databaseSize:databaseInfo.size,backupCount:backups.length,lastBackupAt:backups[0]?.createdAt||null,diskFree:disk.bavail*disk.bsize,diskTotal:disk.blocks*disk.bsize,backupSettings:{enabled:setting("backup_enabled")==="1",frequency:setting("backup_frequency")==="weekly"?"weekly":"daily",weekday:Number(setting("backup_weekday"))||0,hour:Number(setting("backup_hour"))||0,retention:Number(setting("backup_retention"))||14,lastScheduledBackup:setting("last_scheduled_backup")||null,nextScheduledBackup:nextAutomaticBackup()}})}
if(url.pathname==="/api/admin/backup-settings"&&req.method==="PATCH"){const v=await body(req),enabled=Boolean(v.enabled),frequency=String(v.frequency||"daily"),weekday=Number(v.weekday),hour=Number(v.hour),retention=Number(v.retention);if(!new Set(["daily","weekly"]).has(frequency)||!Number.isInteger(weekday)||weekday<0||weekday>6||!Number.isInteger(hour)||hour<0||hour>23||!Number.isInteger(retention)||retention<1||retention>365)throw new Error("Choose a valid frequency, weekday, backup hour, and retention period.");saveSetting("backup_enabled",enabled?"1":"0");saveSetting("backup_frequency",frequency);saveSetting("backup_weekday",weekday);saveSetting("backup_hour",hour);saveSetting("backup_retention",retention);await pruneScheduledBackups();audit(req,user,"backup_schedule_updated","",`${enabled?"enabled":"disabled"}; ${frequency}${frequency==="weekly"?` day ${weekday}`:""}; ${hour}:00; retain ${retention}`);return json(res,200,{ok:true,nextScheduledBackup:nextAutomaticBackup()})} if(url.pathname==="/api/admin/backup-settings"&&req.method==="PATCH"){const v=await body(req),enabled=Boolean(v.enabled),frequency=String(v.frequency||"daily"),weekday=Number(v.weekday),hour=Number(v.hour),retention=Number(v.retention);if(!new Set(["daily","weekly"]).has(frequency)||!Number.isInteger(weekday)||weekday<0||weekday>6||!Number.isInteger(hour)||hour<0||hour>23||!Number.isInteger(retention)||retention<1||retention>365)throw new Error("Choose a valid frequency, weekday, backup hour, and retention period.");saveSetting("backup_enabled",enabled?"1":"0");saveSetting("backup_frequency",frequency);saveSetting("backup_weekday",weekday);saveSetting("backup_hour",hour);saveSetting("backup_retention",retention);await pruneScheduledBackups();audit(req,user,"backup_schedule_updated","",`${enabled?"enabled":"disabled"}; ${frequency}${frequency==="weekly"?` day ${weekday}`:""}; ${hour}:00; retain ${retention}`);return json(res,200,{ok:true,nextScheduledBackup:nextAutomaticBackup()})}
if(url.pathname==="/api/admin/warranties"&&req.method==="GET")return json(res,200,allWarranties()); if(url.pathname==="/api/admin/warranties"&&req.method==="GET")return json(res,200,allWarranties());
if(url.pathname==="/api/admin/warranties"&&req.method==="POST"){const v=await body(req),name=cleanWarrantyName(v.name),durationValue=Number(v.durationValue),durationUnit=String(v.durationUnit||"days");if(!Number.isInteger(durationValue)||durationValue<0||durationValue>3650||!new Set(["days","months","years"]).has(durationUnit))throw new Error("Enter a valid warranty duration.");const id=crypto.randomUUID();db.prepare("INSERT INTO warranty_presets (id,name,duration_value,duration_unit) VALUES (?,?,?,?)").run(id,name,durationValue,durationUnit);audit(req,user,"warranty_created",name,`${durationValue} ${durationUnit}`);return json(res,201,{id,name,durationValue,durationUnit,active:true,isDefault:false,usageCount:0})} if(url.pathname==="/api/admin/warranties"&&req.method==="POST"){const v=await body(req),name=cleanWarrantyName(v.name),durationValue=Number(v.durationValue),durationUnit=String(v.durationUnit||"days");if(!Number.isInteger(durationValue)||durationValue<0||durationValue>3650||!new Set(["days","months","years"]).has(durationUnit))throw new Error("Enter a valid warranty duration.");const id=crypto.randomUUID();db.prepare("INSERT INTO warranty_presets (id,name,duration_value,duration_unit) VALUES (?,?,?,?)").run(id,name,durationValue,durationUnit);audit(req,user,"warranty_created",name,`${durationValue} ${durationUnit}`);return json(res,201,{id,name,durationValue,durationUnit,active:true,isDefault:false,usageCount:0})}
@@ -150,7 +150,7 @@ async function adminApi(req,res,url,session){
} }
async function api(req,res,url){ async function api(req,res,url){
requireOrigin(req);const session=authorize(req,url);if(url.pathname==="/api/health")return json(res,200,{ok:true}); requireOrigin(req);const session=authorize(req,url);if(url.pathname==="/api/health")return json(res,200,{ok:true,appVersion:APP_VERSION,buildSha:BUILD_SHORT_SHA,buildDate:BUILD_DATE});
if(url.pathname.startsWith("/api/auth/")){const handled=await authApi(req,res,url,session);if(handled!==false)return handled} if(url.pathname.startsWith("/api/auth/")){const handled=await authApi(req,res,url,session);if(handled!==false)return handled}
if(url.pathname.startsWith("/api/admin/")){const handled=await adminApi(req,res,url,session);if(handled!==false)return handled} if(url.pathname.startsWith("/api/admin/")){const handled=await adminApi(req,res,url,session);if(handled!==false)return handled}
const user=session.user; const user=session.user;
+3 -3
View File
@@ -1,9 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<Container version="2"> <Container version="2">
<Name>vBoxStock</Name><Repository>ghcr.io/mfwadejr/vboxstock:latest</Repository><Registry>https://github.com/mfwadejr/vboxstock/pkgs/container/vboxstock</Registry> <Name>vBoxStock</Name><Repository>git.us2plus2.com/marvin/vboxstock:latest</Repository><Registry>https://git.us2plus2.com/marvin/vboxstock</Registry>
<Network>bridge</Network><Shell>sh</Shell><Privileged>false</Privileged> <Network>bridge</Network><Shell>sh</Shell><Privileged>false</Privileged>
<Support>https://github.com/mfwadejr/vboxstock/issues</Support><Project>https://github.com/mfwadejr/vboxstock</Project><Overview>Self-contained vSeeBox inventory and sales tracker with an embedded SQLite database.</Overview> <Support>https://git.us2plus2.com/marvin/vboxstock/issues</Support><Project>https://git.us2plus2.com/marvin/vboxstock</Project><Overview>Self-contained vSeeBox inventory and sales tracker with an embedded SQLite database.</Overview>
<Category>Tools:</Category><WebUI>http://[IP]:[PORT:3000]/</WebUI><TemplateURL>https://raw.githubusercontent.com/mfwadejr/vboxstock/main/vboxstock-unraid.xml</TemplateURL><Icon>https://raw.githubusercontent.com/mfwadejr/vboxstock/main/public/assets/vboxstock-icon-512.png</Icon> <Category>Tools:</Category><WebUI>http://[IP]:[PORT:3000]/</WebUI><TemplateURL>https://git.us2plus2.com/marvin/vboxstock/raw/branch/main/vboxstock-unraid.xml</TemplateURL><Icon>https://git.us2plus2.com/marvin/vboxstock/raw/branch/main/public/assets/vboxstock-icon-512.png</Icon>
<Config Name="Web UI Port" Target="3000" Default="8269" Mode="tcp" Description="Recommended vBoxStock host port; maps to container port 3000" Type="Port" Display="always" Required="true" Mask="false">8269</Config> <Config Name="Web UI Port" Target="3000" Default="8269" Mode="tcp" Description="Recommended vBoxStock host port; maps to container port 3000" Type="Port" Display="always" Required="true" Mask="false">8269</Config>
<Config Name="App Data" Target="/data" Default="/mnt/user/appdata/vboxstock" Mode="rw" Description="SQLite database and durable application data" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/vboxstock</Config> <Config Name="App Data" Target="/data" Default="/mnt/user/appdata/vboxstock" Mode="rw" Description="SQLite database and durable application data" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/vboxstock</Config>
<Config Name="User ID" Target="PUID" Default="99" Mode="" Description="Host user ID used to own and write application data. Unraid default is 99 (nobody)." Type="Variable" Display="advanced" Required="true" Mask="false">99</Config> <Config Name="User ID" Target="PUID" Default="99" Mode="" Description="Host user ID used to own and write application data. Unraid default is 99 (nobody)." Type="Variable" Display="advanced" Required="true" Mask="false">99</Config>