Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b941865ae | |||
| a3c565b6ee | |||
| ea677286f0 | |||
| 8317118348 | |||
| 0e87f5a26a | |||
| 60c2af2fde |
+6
-3
@@ -4,10 +4,13 @@ LABEL org.opencontainers.image.description="Self-contained vSeeBox inventory and
|
||||
WORKDIR /app
|
||||
COPY package.json server.mjs ./
|
||||
COPY public ./public
|
||||
RUN mkdir -p /data && chown -R node:node /app /data
|
||||
ENV PORT=3000 DATA_DIR=/data NODE_ENV=production
|
||||
USER node
|
||||
COPY vboxstock-entrypoint.sh /usr/local/bin/vboxstock-entrypoint
|
||||
RUN apk add --no-cache su-exec \
|
||||
&& chmod +x /usr/local/bin/vboxstock-entrypoint \
|
||||
&& mkdir -p /data
|
||||
ENV PORT=3000 DATA_DIR=/data NODE_ENV=production PUID=99 PGID=100
|
||||
EXPOSE 3000
|
||||
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
|
||||
ENTRYPOINT ["/usr/local/bin/vboxstock-entrypoint"]
|
||||
CMD ["node", "server.mjs"]
|
||||
|
||||
@@ -76,13 +76,15 @@ docker run -d \
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-e TZ=America/New_York \
|
||||
-e PUID=99 \
|
||||
-e PGID=100 \
|
||||
-v /your/persistent/path:/data \
|
||||
ghcr.io/mfwadejr/vboxstock:latest
|
||||
```
|
||||
|
||||
Open `http://YOUR-SERVER-IP:3000`, sign in with the initial credentials above, and change the password when prompted.
|
||||
|
||||
The host path mounted at `/data` is essential. Removing the container is safe when this mount remains intact; running without a persistent mount means the database can be lost when the container is replaced.
|
||||
The host path mounted at `/data` is essential. Removing the container is safe when this mount remains intact; running without a persistent mount means the database can be lost when the container is replaced. `PUID` and `PGID` determine which host user and group own the mounted data. The defaults are Unraid's `nobody:users` IDs, `99:100`.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
@@ -110,8 +112,19 @@ Use the included `vboxstock-unraid.xml` template or create a container with thes
|
||||
| Container data path | `/data` |
|
||||
| Suggested Unraid host path | `/mnt/user/appdata/vboxstock` |
|
||||
| Network mode | `bridge` |
|
||||
| Timezone (`TZ`) | `America/New_York` |
|
||||
| PUID | `99` |
|
||||
| PGID | `100` |
|
||||
|
||||
Open the container's WebUI after installation. Updates can be applied with **Force Update** or through the CA Auto Update Applications plugin.
|
||||
Add `TZ`, `PUID`, and `PGID` as Unraid container variables:
|
||||
|
||||
| Name | Key | Value |
|
||||
| --- | --- | --- |
|
||||
| Timezone | `TZ` | `America/New_York` |
|
||||
| User ID | `PUID` | `99` |
|
||||
| Group ID | `PGID` | `100` |
|
||||
|
||||
Open the container's WebUI after installation. Updates can be applied with **Force Update** or through the CA Auto Update Applications plugin. At startup, the container creates `/data/backups`, applies the configured `PUID` and `PGID` ownership to `/data`, and then runs the application with those IDs.
|
||||
|
||||
## Data and backups
|
||||
|
||||
@@ -120,6 +133,10 @@ vBoxStock uses SQLite and does not require MySQL, PostgreSQL, Redis, or another
|
||||
- `/data/vboxstock.db` — active application database
|
||||
- `/data/backups/` — locally retained database snapshots
|
||||
|
||||
`/data` is the path inside the container. When `/mnt/user/appdata/vboxstock` is mapped directly to `/data`, the Unraid host folder contains `vboxstock.db` and `backups/`; it does not contain another nested folder named `data`.
|
||||
|
||||
A fresh installation creates an empty inventory, sales history, and customer list. Only the initial `admin` account is created. Replacing or upgrading the container preserves existing records because `vboxstock.db` remains in the mounted host folder. Removing or changing the `/data` mapping starts a separate empty database, so keep that mapping consistent across upgrades.
|
||||
|
||||
The Admin page can create a transactionally consistent snapshot, download it to another device, restore a local snapshot, or upload and restore a downloaded copy. A pre-restore snapshot is created automatically before the active database is replaced.
|
||||
|
||||
Backups contain customer information and password hashes. Store downloaded copies securely. Restoring a database also restores the user accounts contained in that backup and signs out every active session. An older backup without user accounts starts the first-login `admin` / `admin` setup flow.
|
||||
@@ -143,6 +160,7 @@ docker exec -e RESET_ADMIN_PASSWORD=NewPassword123 -it vboxstock node server.mjs
|
||||
- Image: `ghcr.io/mfwadejr/vboxstock:latest`
|
||||
- Application port: `3000/tcp`
|
||||
- Persistent volume: `/data`
|
||||
- Runtime ownership: configurable with `PUID` and `PGID` (`99:100` by default)
|
||||
- Health check: `GET /api/health`
|
||||
- Runtime: Node.js 22
|
||||
- Database: SQLite
|
||||
|
||||
@@ -9,3 +9,5 @@ services:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PUID=99
|
||||
- PGID=100
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 51 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vboxstock",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": ">=22.13.0" },
|
||||
|
||||
@@ -30,8 +30,6 @@ function validPassword(value){if(String(value||"").length<8)throw new Error("Pas
|
||||
function cleanUsername(value){const name=String(value||"").trim();if(!/^[a-zA-Z0-9._-]{3,40}$/.test(name))throw new Error("Username must be 3–40 characters using letters, numbers, periods, dashes, or underscores.");return name}
|
||||
initializeDatabase();
|
||||
try{const event=JSON.parse(await readFile(restoreMarker,"utf8"));db.prepare("INSERT INTO audit_log (username,action,target,details,ip_address) VALUES (?,'database_restore',?,?,?)").run(event.username||"system",event.target||"",event.details||"Restored database",event.ipAddress||"");await unlink(restoreMarker)}catch(error){if(error.code!=="ENOENT")console.error("Unable to import restore audit event:",error.message)}
|
||||
const seed=[["Logan Crabtree","273D000000021255","A0:BB:3E:02:12:55"],["Marvin Wade","273D00000002194E","A0:BB:3E:02:19:4E"],["Logan Crabtree","273D00000002185F","A0:BB:3E:02:18:5F"],["Logan Crabtree","273D00000002193A","A0:BB:3E:02:19:3A"],["Mark Milburn","273D0000000218BD","A0:BB:3E:02:18:BD"],["Matt Avila","273D0000000219D5","A0:BB:3E:02:19:D5"],["Andy Nguyen","273D0000000219FD","A0:BB:3E:02:19:FD"],["Andy Nguyen","273D000000021181","A0:BB:3E:02:11:81"],["Daniel Wade","273D00000002125A","A0:BB:3E:02:12:5A"],["Marvin Wade","273D0000000219D4","A0:BB:3E:02:19:D4"]];
|
||||
if(db.prepare("SELECT COUNT(*) count FROM products").get().count===0){const insert=db.prepare("INSERT INTO products (id,uid,sn,mac,model,condition,received_at,status,sold_at,customer_name) VALUES (?,?,?,?,?,?,?,?,?,?)");db.exec("BEGIN");try{seed.forEach(([name,sn,mac])=>insert.run(crypto.randomUUID(),"",sn,mac,"V3 Plus","New","2024-08-20","sold","2024-08-20",name));db.exec("COMMIT")}catch(e){db.exec("ROLLBACK");throw e}}
|
||||
if(db.prepare("SELECT COUNT(*) count FROM users").get().count===0){db.prepare("INSERT INTO users (id,username,password_hash,role,must_change_password) VALUES (?,?,?,?,1)").run(crypto.randomUUID(),"admin",hashPassword("admin"),"admin");db.prepare("INSERT INTO audit_log (action,target,details) VALUES ('bootstrap_admin','admin','Default administrator created; password change required')").run()}
|
||||
const findCustomerByName=db.prepare("SELECT id FROM customers WHERE lower(name)=lower(?) ORDER BY updated_at DESC LIMIT 1"),addCustomer=db.prepare("INSERT INTO customers (id,name,phone,address1,address2,city,state,zip,shipping_notes) VALUES (?,?,?,?,?,?,?,?,?)");
|
||||
for(const old of db.prepare("SELECT DISTINCT customer_name name,phone FROM products WHERE status='sold' AND customer_name IS NOT NULL AND customer_name!='' AND customer_id IS NULL").all()){let customer=findCustomerByName.get(old.name);if(!customer){const id=crypto.randomUUID();addCustomer.run(id,old.name,old.phone||"","","","","","","");customer={id}}db.prepare("UPDATE products SET customer_id=? WHERE status='sold' AND customer_id IS NULL AND lower(customer_name)=lower(?)").run(customer.id,old.name)}
|
||||
|
||||
@@ -23,7 +23,7 @@ test("authentication, roles, inventory, sale, and restock", async t => {
|
||||
response=await request("/api/auth/change-password",{method:"POST",headers:jsonHeaders(bootstrap.cookie),body:JSON.stringify({currentPassword:"admin",newPassword:"password8",confirmPassword:"password8"})});assert.equal(response.status,200);
|
||||
const adminCookie=response.headers.get("set-cookie").split(";")[0];
|
||||
|
||||
response=await request("/api/products",{headers:{cookie:adminCookie}});let items=await response.json();assert.equal(items.length,10);
|
||||
response=await request("/api/products",{headers:{cookie:adminCookie}});let items=await response.json();assert.equal(items.length,0,"a fresh database must contain no inventory or sales");
|
||||
response=await request("/api/admin/users",{method:"POST",headers:jsonHeaders(adminCookie),body:JSON.stringify({username:"viewer",password:"viewer123",role:"readonly"})});assert.equal(response.status,201);
|
||||
const viewerLogin=await login("viewer","viewer123");
|
||||
response=await request("/api/auth/change-password",{method:"POST",headers:jsonHeaders(viewerLogin.cookie),body:JSON.stringify({currentPassword:"viewer123",newPassword:"viewer456",confirmPassword:"viewer456"})});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
puid="${PUID:-99}"
|
||||
pgid="${PGID:-100}"
|
||||
|
||||
case "$puid:$pgid" in
|
||||
*[!0-9:]*|:*|*:) echo "PUID and PGID must be numeric." >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
mkdir -p "${DATA_DIR:-/data}" "${DATA_DIR:-/data}/backups"
|
||||
chown -R "$puid:$pgid" "${DATA_DIR:-/data}"
|
||||
|
||||
exec su-exec "$puid:$pgid" "$@"
|
||||
@@ -6,5 +6,7 @@
|
||||
<Category>Tools:</Category><WebUI>http://[IP]:[PORT:3000]/</WebUI><TemplateURL></TemplateURL><Icon></Icon>
|
||||
<Config Name="Web UI Port" Target="3000" Default="3000" Mode="tcp" Description="vBoxStock web interface" Type="Port" Display="always" Required="true" Mask="false">3000</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="Group ID" Target="PGID" Default="100" Mode="" Description="Host group ID used to own and write application data. Unraid default is 100 (users)." Type="Variable" Display="advanced" Required="true" Mask="false">100</Config>
|
||||
<Config Name="Timezone" Target="TZ" Default="America/New_York" Mode="" Description="Container timezone" Type="Variable" Display="advanced" Required="false" Mask="false">America/New_York</Config>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user