infrastructure/server_nas08.md
2026-07-26 22:12:38 -04:00

1018 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NAS08 — Storage, DNS, and Application Server
Device Type: **Raspberry Pi 5 — 8GB**
Hostname: **NAS08**
IP Address: **192.168.150.35**
VLAN: **50 — Lab / Servers**
Last Updated: 2026-07-25
---
## 🧩 Role & Purpose
NAS08 is a **multiservice application server** providing DNS, storage, media hosting, and selfhosted applications for the KingDezigns network.
It is the **Pihole DNS provider** for all VLANs and also hosts several Dockerbased services including Nextcloud, Vaultwarden, and nginx.
This device is a **critical infrastructure component**, supporting both internal services and Home Assistant integrations.
> Note: Plex Media Server was migrated off NAS08 to **PLEX32** (dedicated Dell Wyse 5070) on 2026-06-18 to support advanced Plex Pass features (Sonic Radio) requiring more CPU capability. The Plex container remains present on NAS08 but is permanently disabled (`restart: no`). Media files remain on NAS08 — PLEX32 accesses them via NFS. See `server_plex32.md`.
---
## 🖥️ Hardware
- Raspberry Pi 5 (8GB RAM)
- SSD storage (see drive inventory below)
- Gigabit Ethernet
### **Drive Inventory**
| Device | Model | Capacity | Role |
|--------|-------|----------|------|
| /dev/sda | PNY CS900 | 500GB | OS / system drive |
| /dev/sdb | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data |
| /dev/sdc | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data |
| /dev/sdd | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data |
| /dev/sde | Crucial BX500 (CT1000BX500SSD1) | 1TB | Data |
---
## 🌐 Network Placement
- VLAN: **50 — Lab / Servers**
- IP: **192.168.150.35**
- Access Type: LAN / Cable
This placement ensures:
- Local DNS availability for all VLANs
- Isolation from trusted user devices
- Controlled access to sensitive applications
- Reduced attack surface
### **DNS Resolver Configuration (Host-level)**
NAS08 runs `systemd-resolved`. The host's `eth0` interface correctly uses Pihole (192.168.150.35) as its only DNS server via DHCP.
> ⚠️ **Known issue, fixed 2026-07-18:** `/etc/systemd/resolved.conf` previously had a manually-set global override:
> ```
> [Resolve]
> DNS=1.1.1.1 8.8.8.8
> FallbackDNS=9.9.9.9
> ```
> Because `resolv.conf mode` is `uplink`, this **global** `DNS=` line merged with the correct per-interface Pihole entry, causing `/etc/resolv.conf` to list all three nameservers. This produced inconsistent/duplicate DNS answers (e.g. a hostname resolving to both its correct internal IP *and* the public WAN IP), which broke local-DNS-override-dependent services such as ONLYOFFICE (see below).
>
> **Fix applied:** removed the global `DNS=` line from `/etc/systemd/resolved.conf`, leaving `FallbackDNS=9.9.9.9` in place as a last-resort fallback only. Verify with `resolvectl status` — the `Global` block should show no `DNS Servers:` line, and `Link 2 (eth0)` should show only `192.168.150.35`.
>
> This fix required a container restart (or host reboot) for already-running Docker containers to pick up the corrected host resolver, since Docker's embedded DNS (127.0.0.11) inherits the host's `resolv.conf` at container start.
---
## 📦 Primary Functions
### **Core Services**
- **Pihole DNS** (Docker)
- **Nextcloud** (Docker) — with ONLYOFFICE document editing (see dedicated section below)
- **Vaultwarden** (Docker)
- **nginx** (Docker, disabled except for backup use)
### **Additional Roles**
- Home Assistant backup storage
- General NAS storage
- Internal web services
- **PLEX32 config backup destination** — receives nightly tar archives from PLEX32 via NFS
---
## 📝 Nextcloud — Office / Document Editing (ONLYOFFICE)
### **Overview**
Nextcloud on NAS08 uses **ONLYOFFICE Document Server (Community Edition, free)** for in-browser editing of Word/Excel/PowerPoint-compatible files (.docx/.xlsx/.pptx/.odt etc.), providing a Google Docs/MS Officestyle collaborative editing experience.
Only **one** Office integration app is active. Nextcloud supports multiple competing Office apps simultaneously, which caused a multi-day outage (see Troubleshooting History below) — **do not enable more than one Office/document-editing app at a time.**
### **Current Configuration**
| Component | Value |
|---|---|
| Nextcloud app in use | `onlyoffice` (official Ascensio app) |
| Document Server container | `onlyoffice` (compose service name) — image `onlyoffice/documentserver:latest` |
| Container host | NAS08, part of the `nextcloud` docker-compose project |
| Internal port | 9980 → container port 80 |
| Public URL | `https://office.kingdezigns.com/` (proxied via NPM on HAS) |
| Internal callback URL | `http://onlyoffice:80/` (Docker service-name resolution within `nextcloud_net`) |
| JWT authentication | **Enabled** — shared secret configured on both the Nextcloud `onlyoffice` app settings and the container's `JWT_SECRET` env var |
| Edition | Community Edition — free, capped at 20 simultaneous editing connections (more than sufficient for home/small network use) |
### **Disabled Office Apps (do not re-enable simultaneously with `onlyoffice`)**
| App | Reason disabled |
|---|---|
| `eurooffice` | Third-party wrapper around the same ONLYOFFICE engine — redundant, was misconfigured (`StorageUrl` DNS issue) |
| `officeonline` | Legacy Microsoft WOPI/Office Online connector — largely deprecated for self-hosting, historically required MS volume licensing |
| `office` | Nextcloud's newer bundled Office app — less mature, no backend configured |
### **Docker Compose Environment (onlyoffice service)**
Managed via **OMV → Services → Compose** (do not hand-edit the generated `.yml` files directly — changes will be overwritten by OMV):
```yaml
onlyoffice:
image: onlyoffice/documentserver:latest
container_name: onlyoffice
restart: unless-stopped
ports:
- "9980:80"
environment:
- JWT_ENABLED=true
- JWT_SECRET=<secret — stored in Vaultwarden, not in plaintext docs>
- ALLOW_PRIVATE_IP_ADDRESS=true
networks:
- nextcloud_net
```
**Key environment variables:**
- `ALLOW_PRIVATE_IP_ADDRESS=true`**required**. Without this, the Document Server's built-in SSRF protection rejects callback requests to Nextcloud's private LAN IP/hostname, causing "Error while downloading the document file to be converted."
- `JWT_ENABLED=true` + `JWT_SECRET=...` — requires every request between Nextcloud and the Document Server to be signed. Without a matching secret on both sides, requests fail with a silent `{"message":"Access denied"}` from Nextcloud's `onlyoffice` app — this does **not** appear in the standard `nextcloud.log` PHP-level log; it only shows in the Apache/container stdout log (`docker logs nextcloud`) and in `nextcloud.log` if you grep specifically for `"app":"onlyoffice"`.
### **Pi-hole Local DNS Requirement**
The ONLYOFFICE Document Server container must reach Nextcloud at its **public hostname** (`cloud.kingdezigns.com`) to download/upload files, because that's the `StorageUrl` Nextcloud is configured with (`overwritehost` = `cloud.kingdezigns.com`). Without a local DNS override, this hostname resolves to the WAN IP, causing a NAT hairpin request that typically fails.
**Required Pi-hole Local DNS Record:**
| Domain | IP |
|---|---|
| `cloud.kingdezigns.com` | `192.168.150.30` |
Configured under Pi-hole Admin → Local DNS → DNS Records. This also improves performance for all other internal clients accessing Nextcloud by its public domain name.
### **Verification Commands**
```bash
# Confirm only onlyoffice app is enabled (not eurooffice/officeonline/office)
sudo docker exec -u www-data nextcloud php occ app:list | grep -i office
# Confirm JWT secret is set and matches on both sides
sudo docker exec -u www-data nextcloud php occ config:app:get onlyoffice jwt_secret
sudo docker inspect onlyoffice --format '{{json .Config.Env}}' | tr ',' '\n' | grep JWT
# Confirm container can resolve and reach Nextcloud internally
sudo docker exec onlyoffice getent hosts cloud.kingdezigns.com # should return ONLY 192.168.150.30
sudo docker exec onlyoffice curl -sk -o /dev/null -w "%{http_code}\n" https://cloud.kingdezigns.com/
# Watch live Document Server logs during a test document open
sudo docker logs -f onlyoffice --tail 0
```
### **Troubleshooting History (2026-07-17/18)**
Office editing broke silently after previously working. Root causes, in the order they were found and fixed:
1. **Four Office apps were installed simultaneously** (`onlyoffice`, `eurooffice`, `officeonline`, `office`), causing Nextcloud to route file-open actions unpredictably. Resolved by disabling all but `onlyoffice`.
2. **Host DNS split-brain** — a stray global `DNS=1.1.1.1 8.8.8.8` in `/etc/systemd/resolved.conf` caused `cloud.kingdezigns.com` to resolve to both the correct internal IP and the public WAN IP, producing a NAT hairpin failure. Fixed by removing the global override (see DNS section above) and adding a Pi-hole local DNS record.
3. **SSRF private-IP block** — the Document Server refused to call back to Nextcloud's private IP by default. Fixed with `ALLOW_PRIVATE_IP_ADDRESS=true`.
4. **JWT mismatch** — Nextcloud had a JWT secret configured while the container had `JWT_ENABLED=false`, causing every Document Server callback to be silently rejected with `{"message":"Access denied"}` — invisible in normal logs, only visible via Apache-level container stdout log or a targeted grep of `nextcloud.log`. Fixed by aligning JWT config on both sides (currently: enabled, with a shared secret).
**Diagnostic lesson learned:** Nextcloud's PHP-level `nextcloud.log` does **not** capture ONLYOFFICE app-level "Access denied" rejections by default in a way that's easy to spot — always check `docker logs nextcloud` (Apache access log) directly for the actual HTTP status code of the failing request, then grep `nextcloud.log` for the specific `app":"onlyoffice"` entries at that timestamp.
---
## 📈 STOCKPROXY — Self-Hosted Stock/Fund Price API
### **Overview**
As of 2026-07-21, NAS08 runs **STOCKPROXY**, a small self-hosted Flask API that supplies
current and historical stock/mutual-fund prices to ONLYOFFICE spreadsheets. It was built
because ONLYOFFICE has no equivalent to Excel's `STOCKHISTORY()`/linked stock data types,
and neither of the obvious alternatives worked on their own:
- **Finnhub** (free tier) has no mutual fund NAV coverage at all — confirmed via direct
testing, returns `{"error":"no data"}` for every mutual fund ticker tried (MKDVX, FIFRX,
etc.). Finnhub is still used directly (not through this proxy) for regular equities via
separate ONLYOFFICE custom functions on the "Dashboard" tab.
- **Yahoo Finance** has good mutual fund coverage (current + historical) but explicitly
blocks direct browser-based (CORS) requests — confirmed by Yahoo Finance library
maintainers themselves ("we will not help you bypass" the block).
STOCKPROXY solves this by calling Yahoo Finance **server-to-server** (no CORS restriction
applies between servers), then re-serving the result to the browser with permissive CORS
headers so an ONLYOFFICE custom function running client-side can call it directly.
Used by the "HSA Investiments Choices" tab of `Rufus Retirement Account Tracking 2026.xlsx`
to track ~50 mutual funds. See that workbook's own documentation for the spreadsheet-side
custom functions (`FUNDPRICE`, `FUNDPRICE_HIST`) and cell layout.
### **Current Configuration**
| Component | Value |
|---|---|
| Container name | `stockproxy` (compose service name) |
| Container host | NAS08, standalone Docker Compose project at `~/docker/stockproxy/` |
| Internal port | 5005 (host and container match) |
| Public URL | `https://stocks.kingdezigns.com/` (proxied via NPM on HAS — see `server_homeassistant.md` Proxy Hosts table) |
| Data source | Yahoo Finance unofficial chart API (`query1.finance.yahoo.com/v8/finance/chart/{ticker}`), scraped server-side with a browser-like `User-Agent` header — no API key required |
| Auth | Shared-secret `key` query parameter on every price endpoint — confirmed rejects requests without it (`{"error":"unauthorized"}`). Secret stored in Vaultwarden, generated via `openssl rand -hex 16` |
| CORS | `Access-Control-Allow-Origin: *` set explicitly — required because ONLYOFFICE custom functions call `fetch()` from the browser tab, not from a server |
### **Endpoints**
| Endpoint | Params | Behavior |
|---|---|---|
| `/current` | `ticker`, `key` | Today's price. In-memory cache, 15-minute TTL (mutual funds price once/day after close; short cache lets same-day NAV postings show up without waiting a full day). |
| `/historical` | `ticker`, `date` (YYYY-MM-DD), `key` | Closing price on/before the given date — resolves to the nearest prior trading day to survive weekends/holidays. Cached **permanently** in SQLite once resolved, since a past date's close never changes. If `date` is today or in the future, transparently falls back to `/current` logic. |
| `/health` | none | Basic up/down check, no auth required. |
### **Docker Compose Project — stockproxy**
Located at:
```
~/docker/stockproxy/
```
Files: `app.py`, `requirements.txt`, `Dockerfile`, `docker-compose.yml`.
```yaml
services:
stockproxy:
build: .
container_name: stockproxy
restart: unless-stopped
ports:
- "5005:5005"
environment:
- PROXY_SECRET=<secret — stored in Vaultwarden, not in plaintext docs>
- DB_PATH=/data/cache.db
volumes:
- /opt/stockproxy/data:/data
```
**Key notes:**
- `Dockerfile` builds on `python:3.12-slim`, installs `flask`/`requests`/`gunicorn`, runs via
`gunicorn --bind 0.0.0.0:5005 --workers 2 --timeout 30 app:app` (production WSGI server,
not Flask's dev server).
- Data volume `/opt/stockproxy/data` on the host (owned by `rufusking`, created via
`sudo mkdir` + `chown` back to the normal user — same permission pattern used elsewhere on
NAS08) holds the permanent SQLite historical-price cache at `cache.db`, so it survives
container rebuilds/restarts.
- No `FINNHUB_API_KEY` variable — Finnhub was dropped from this service entirely after
confirming it has no mutual fund coverage; only Yahoo is used here.
### **NPM / SSL**
Proxied via NPM on HAS — see `server_homeassistant.md` Proxy Hosts table for the entry.
Public DNS (`stocks.kingdezigns.com`, Dreamhost A record) was added to the existing daily
DDNS refresh script alongside the other KingDezigns subdomains.
> **Note:** HTTPS/SSL is required here, not optional — ONLYOFFICE is itself served over
> HTTPS, and browsers block a plain-HTTP `fetch()` call from an HTTPS page as mixed content.
> The proxy host was initially created without SSL, then edited to add **Force SSL** +
> **HTTP/2 Support** once DNS had propagated — same NPM host throughout, not a second one.
### **Verification Commands**
```bash
# Local health check, bypasses NPM/DNS entirely
curl "http://localhost:5005/health"
# Confirm auth is enforced (should return unauthorized, not real data)
curl "https://stocks.kingdezigns.com/current?ticker=MKDVX"
# Real current-price lookup
curl "https://stocks.kingdezigns.com/current?ticker=MKDVX&key=<secret>"
# Real historical lookup
curl "https://stocks.kingdezigns.com/historical?ticker=MKDVX&date=2025-07-31&key=<secret>"
```
### **Known Limitations**
- Yahoo's chart endpoint is unofficial and could change or start blocking the scraping
`User-Agent` pattern at some point — not a documented/supported API. Same risk category as
the PLEX32 `docker pull` intermittent-failure issue: usually reliable, not guaranteed
forever.
- At ~50 tickers, a first-time lookup for a newly-rolled-forward anchor date results in ~50
fresh Yahoo requests in quick succession — not yet stress-tested for Yahoo-side
rate-limiting at that volume. Widespread simultaneous `#ERROR`/`#N/A` across many tickers
(rather than one-off) should be treated as a signal to wait a minute and retry, not a sign
the service is broken.
- Read-only service — no mutation/write endpoints, so no data-integrity risk beyond the
cache itself.
---
## 📈 Collabora Online — Stock/Fund Price Macro Integration (2026-07-22)
### **Overview**
As part of the ongoing ONLYOFFICE vs. Collabora reliability trial (see `network_index.md`),
the ONLYOFFICE custom-function stock price integration was ported to Collabora. Originally
this included `FUNDPRICE`/`FUNDPRICE_HIST` (calling STOCKPROXY) plus `STOCKPRICE`/`STOCKTIME`
(calling Finnhub directly) — **Finnhub was removed on 2026-07-23; see the dedicated section
below.** All four spreadsheet functions now call STOCKPROXY exclusively. The STOCKPROXY
backend itself required zero changes for the Collabora port — it's plain HTTPS/JSON and both
editors can call it identically. Only the spreadsheet-side "wrapper" had to be rewritten,
because ONLYOFFICE and Collabora use completely different macro engines (ONLYOFFICE: JS
`Api.AddCustomFunction()` running in the browser; Collabora/LibreOffice: Basic macros running
server-side inside the Collabora container).
**Status: working as of 2026-07-23.** `FUNDPRICE("SMERY")`, `FUNDPRICE_HIST`, and the new
`FUNDTIME` all confirmed working in the Collabora test copy.
### **File Format Requirement — `.ods`, not `.xlsx`**
The Collabora test copy must be saved as **ODF Spreadsheet (`.ods`)**, not `.xlsx`. LibreOffice
Basic modules do not reliably persist across save/close/reopen cycles in `.xlsx` (OOXML) —
confirmed during setup: the module silently vanished from the file every time it was saved as
`.xlsx` and reopened. Switching to native `.ods` fixed this immediately and the module has
persisted reliably since. The ONLYOFFICE production file is unaffected and remains `.xlsx`
this only applies to the separate Collabora test copy (`stock_tracking_collabora.ods`), which
was always intended to be a separate file per the original trial plan.
### **Macro Module — `StockFunctions.bas`**
LibreOffice Basic port of the ONLYOFFICE custom functions. Stored as a document-embedded Basic
module (Standard library, inside the document itself — not "My Macros"), so it travels with
the `.ods` file.
**Key implementation differences from the ONLYOFFICE JS version:**
- No native `fetch()`/`async` in LibreOffice Basic. HTTP GET is implemented via the
`com.sun.star.ucb.SimpleFileAccess` + `com.sun.star.io.TextInputStream` UNO services,
called synchronously.
- A small hand-rolled `JsonNumber()` helper extracts a numeric value from the flat JSON
response (`{"price": 34.54, ...}`) — no JSON library dependency needed since STOCKPROXY's
response shape is simple and flat. A parallel `JsonString()` helper (added 2026-07-23,
see Finnhub removal section below) extracts quoted string values the same way, for
`resolved_date`.
- A minimal `EncodeUrl()` percent-encoder handles ticker symbols and `yyyy-mm-dd` date strings
in the query string.
- Synchronous execution means a full recalc of a large fund table (e.g. ~50 funds × 2 calls
each) will be noticeably slower than ONLYOFFICE's concurrent browser-side `fetch()` calls —
worth monitoring; STOCKPROXY's 15-minute `/current` cache helps but doesn't eliminate the
round-trip cost per cell.
**Current formula usage** (as of 2026-07-23 — `STOCKPRICE`/`STOCKTIME` removed, see below):
```
=FUNDPRICE(D4)
=FUNDPRICE_HIST(D4,TEXT($B$3,"yyyy-mm-dd"))
=FUNDPRICE(A5) ' equity tickers — replaces old =STOCKPRICE(A5)
=FUNDTIME(A5) ' equity tickers — replaces old =STOCKTIME(A5)
```
**Editing limitation:** Collabora's browser UI (via Nextcloud/`richdocuments`) does not expose
a working Basic IDE — Tools → Macros only offers "Run Macro" (which only lists `Sub`
procedures, not the `Function`s used as spreadsheet formulas — an empty list here is expected,
not an error). Any future edits to `StockFunctions.bas` must be made in **desktop LibreOffice
Calc**, then the `.ods` file re-uploaded to Nextcloud to replace the Collabora copy. Desktop
LibreOffice also requires its own client-side macro security level set to **Medium** (Tools →
Options → LibreOffice → Security → Macro Security) — separate from the server-side setting
below — or macros will be silently blocked with "Macros in this document are disabled due to
the Macro Security settings."
### **Required Collabora Server Config Changes (`coolwsd.xml`)**
Two separate server-side restrictions had to be lifted before the macro would execute. Both
are deliberate security defaults, not bugs — see Security Considerations below.
**1. Macro execution was disabled entirely by default:**
```xml
<enable_macros_execution desc="..." type="bool" default="false">true</enable_macros_execution>
```
`macro_security_level` was left at its default of `1` (Medium) — this means Collabora shows a
one-time "allow macros for this document?" prompt on open, which was intentionally kept rather
than dropping to `0` (no prompt at all).
**2. Network allowlist (`net.lok_allow`) blocked the STOCKPROXY hostname:**
Even with macros enabled, the first real attempt failed with:
```
ERROR 1: An exception occurred Type: com.sun.star.uno.RuntimeException
Message: access to host denied. URL: https://stocks.kingdezigns.com/current?...
```
**Root cause:** `net.lok_allow.host` entries are regex patterns matched against the **literal
URL/hostname string**, not against the DNS-resolved IP. The stock default list only contains
patterns for literal private-IP text (e.g. `192\.168\.[0-9]{1,3}\.[0-9]{1,3}`, `localhost`,
loopback addresses). Because the macro calls `https://stocks.kingdezigns.com/...` (a hostname,
not an IP literal), it never matched any default pattern — even though that hostname resolves
to `192.168.150.30`, a private IP that would have matched if it had been used directly. Calling
the raw IP instead of the hostname was considered and rejected: NPM on HAS routes by hostname,
so an IP-only request would very likely hit the wrong vhost or be rejected by NPM entirely.
**Fix — added a new entry to the existing `<net><lok_allow>` block:**
```xml
<host desc="stockproxy API on NAS08, proxied via NPM on HAS">stocks\.kingdezigns\.com</host>
```
Added directly after the existing `localhost` entry, inside `<lok_allow>`.
**Applying changes to the Collabora container:**
```bash
docker cp collabora:/etc/coolwsd/coolwsd.xml coolwsd.xml
# edit coolwsd.xml
docker cp coolwsd.xml collabora:/etc/coolwsd/coolwsd.xml
docker restart collabora
```
### **Diagnostic Method — Exposing the Real Error**
The macro's normal error handling (`On Error GoTo ErrHandler`) swallows exceptions into a
generic `#ERROR` cell value, which masked the real cause during initial testing. A temporary
diagnostic Sub was used to surface the actual LibreOffice exception message:
```basic
Sub DebugFundPrice
Dim sUrl As String
sUrl = PROXY_BASE() & "/current?ticker=SMERY&key=" & PROXY_SECRET()
Dim oSFA As Object, oStream As Object
On Error GoTo ErrHandler
oSFA = createUnoService("com.sun.star.ucb.SimpleFileAccess")
oStream = oSFA.openFileRead(sUrl)
MsgBox "SUCCESS - stream opened"
Exit Sub
ErrHandler:
MsgBox "ERROR " & Err & ": " & Error$ & Chr(10) & "URL: " & sUrl
End Sub
```
Run via Tools → Run Macro (visible there since it's a `Sub`, unlike the `Function`s). This is
what revealed the `access to host denied` message and made the `net.lok_allow` root cause
identifiable. **Removed from the module after diagnosis was complete** — it embedded the
proxy secret in plaintext inside a `MsgBox` call, which is fine transiently for local
debugging but shouldn't be left in a saved document.
### **Pi-hole Local DNS Record — Verified Working (was previously unconfirmed)**
The STOCKPROXY section above lists a required Pi-hole local DNS record for
`stocks.kingdezigns.com``192.168.150.30`. During this session that record was found to
**not actually be resolving correctly**`dig @192.168.150.35 stocks.kingdezigns.com` was
returning the public WAN IP (`72.238.137.115`) instead of the internal override, causing the
same NAT-hairpin failure pattern previously seen with `cloud.kingdezigns.com` and
`collabora.kingdezigns.com`.
Pi-hole v6 (Docker) does not have a working `pihole restartdns` subcommand from inside the
container (`docker exec pihole pihole restartdns` fails — command doesn't exist in v6).
`docker exec pihole pihole reloaddns` ran but threw a harmless script error
(`local: FTL_PID_FILE: readonly variable`) — it appeared to still flush/reload correctly in
principle, but a full container restart was used to be certain:
```bash
docker restart pihole
```
After the restart, `dig @192.168.150.35 stocks.kingdezigns.com` correctly returned
`192.168.150.30` with the `aa` (authoritative answer) flag set, confirming Pi-hole is now
serving the local override as intended. **If this record is ever found not resolving locally
again, a full `docker restart pihole` is the confirmed reliable fix** — `reloaddns` may not be
sufcient in Pi-hole v6 containers.
### **Diagnostic Note — "Slow Kit jail setup with copying, cannot bind-mount"**
Collabora's own "Server audit" panel (visible in the browser UI) surfaced this as a red-status
warning during troubleshooting. Investigated and **confirmed unrelated to the network/macro
issue** — this is a well-documented, cosmetic, performance-only Collabora Docker issue
(startup self-test for bind-mount support runs as an unprivileged UID and always fails inside
Docker, falling back to slower file-copying instead of bind-mounts for the per-document jail).
It does not affect network access, macro execution, or document correctness. No action taken;
noted here so it isn't re-investigated as a false lead in the future.
### **Security Considerations**
Both `coolwsd.xml` changes above are server-wide, not scoped to a single document — any file
opened in this Collabora instance can now execute embedded macros with network access to the
allowlisted hosts, not just this spreadsheet. This is a deliberate, accepted tradeoff given
Collabora only serves documents from trusted KingDezigns Nextcloud users, but is worth keeping
in mind:
- `enable_macros_execution` was previously hardened to `false` by default following
**CVE-2025-24796** (a macro-based remote code execution vulnerability affecting Collabora's
jailed macro execution model). `macro_security_level` was deliberately left at `1` (Medium,
prompts before running) rather than `0` (always allow, no prompt) to retain a manual
confirmation step.
- The `net.lok_allow` allowlist exists specifically to limit which hosts a jailed macro can
reach, even with execution enabled — only `stocks.kingdezigns.com` was added, not a broader
wildcard.
### **Open / Follow-up Items**
- Recreate the ~50-fund table and ranking table (`RANK.EQ` + `+ROW()/1000000` tie-breaker) in
the Collabora `.ods` copy — plain spreadsheet formulas, expected to carry over unchanged.
- Stress-test full-sheet recalc performance with the complete fund table, given the
synchronous/blocking nature of Basic macro HTTP calls vs. ONLYOFFICE's concurrent JS.
- Re-test conditional formatting on formula cells in Collabora — this was the original bug
that triggered the ONLYOFFICE vs. Collabora trial and has not yet been re-verified on the
Collabora side with the stock-price formulas in place.
- Confirm recalc-on-file-load behavior (Tools → Options → LibreOffice Calc → Formula →
Recalculation on File Load) is set to always recalculate, to avoid stale values on open.
---
## 📊 RANK.EQ Circular Reference (Err:522) Fix — Static Price Snapshot (2026-07-23)
### **Overview**
The "HSA Investiments Choices" tab uses `RANK.EQ` to rank ~50 funds by percent price
change. The ranking worked correctly on file open but threw **Err:522 (circular
reference)** on the rank column any time *any* cell in the sheet was edited — in both
ONLYOFFICE and the Collabora `.ods` test copy.
### **Root Cause**
The rank formula's range ultimately depended on live, network-backed custom function
calls (`FUNDPRICE`/`FUNDPRICE_HIST`, calling STOCKPROXY — see dedicated sections above).
The chain was:
```
P = FUNDPRICE(ticker) ← live network call
Q = FUNDPRICE_HIST(ticker, date) or fallback FUNDPRICE ← live network call
S = (P - historical) / P → % change ← depends on P, Q
RANK.EQ(S4, $S$3:$S$53, 0) ← depends on all 50 S cells
```
`RANK.EQ` was not literally self-referential (it lived in a separate column, not inside
`S3:S53`), and swapping it for an equivalent `SUMPRODUCT(($S$3:$S$53>S4))+1` formula
produced the identical Err:522 — which ruled out the ranking function itself as the
cause. The actual problem: on file *load*, the sheet calculates once, top-to-bottom, and
everything settles. On an *edit-triggered* incremental recalc, the engine has to confirm
all 50 live/async price-fetch cells have resolved before it can evaluate a formula that
reads the whole range at once. If any of those cells is still "in flight" when the rank
formula re-evaluates, the dependency graph re-enters a cell that's mid-calculation and the
engine reports it as circular reference rather than "value pending."
**Confirmed via isolated test:** pointing a rank formula at a column of plain static
numbers (no formulas at all) instead of the live `S` column did **not** error on edit —
this isolated the cause to the live/async dependency chain, not the rank formula syntax.
### **Fix — Manual Snapshot Macro**
Rather than ranking directly against the live `FUNDPRICE`/`FUNDPRICE_HIST` chain, a
button-triggered macro copies the **calculated values** (not formulas) of the live price
columns into new static columns, and all downstream percent-change/rank formulas were
rebuilt to read from the static columns instead.
**Column layout (spreadsheet-side):**
| Column | Contents |
|---|---|
| `P` | Live: `=FUNDPRICE(N3)` — current price (unchanged) |
| `Q` | Live: `=IF(N3="","",IF(FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))="#N/A",FUNDPRICE(N3),FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))))` — historical price (unchanged) |
| `R` | Static snapshot of `P`, values only, written by macro |
| `S` | Static snapshot of `Q`, values only, written by macro |
| `T` | `=R3-S3` — dollar difference, now built from static inputs |
| `U` | `=T3/R3` — percent change, calculated from static inputs |
| Rank column | `=RANK.EQ(U4,$U$3:$U$53,0)` — now depends only on the static chain |
`P` and `Q` are untouched and continue to refresh live/normally whenever the sheet
recalculates. Only `R`/`S` are static, and they are only ever refreshed when the macro is
explicitly run — decoupling the rank calculation from the live network-fetch chain
entirely, so ordinary cell edits no longer trigger Err:522.
**Working macro (LibreOffice Basic, run via a sheet button):**
```basic
REM ***** BASIC *****
Sub CopyStockValues
Dim oDoc As Object
Dim oSheet As Object
Dim oSource As Object
Dim oDest As Object
oDoc = ThisComponent
oSheet = oDoc.CurrentController.ActiveSheet
' Source: P3:Q53
oSource = oSheet.getCellRangeByName("P3:Q53")
' Destination: R3:S53
oDest = oSheet.getCellRangeByName("R3:S53")
' Copy only the calculated values (no formulas or formatting)
oDest.setDataArray(oSource.getDataArray())
End Sub
```
Bound to a sheet button so the user can trigger a refresh on demand. Since mutual fund
NAVs only post once per day after market close (per the `FUNDPRICE` behavior documented
above), a once- or twice-daily manual click is sufficient — the sheet can otherwise be
freely edited all day without the rank column breaking.
### **Diagnostic Dead Ends / Notes for Future Troubleshooting**
- **`ThisComponent.Sheets.getByIndex(0)` is not reliable** if the workbook has multiple
tabs — an early draft of the macro wrote to sheet index 0 while the user was viewing a
different tab, silently "succeeding" (no error, message box fired) while writing to the
wrong (effectively invisible) location. Use
`oDoc.CurrentController.ActiveSheet` instead to always target the tab currently in view.
- **Recalculation-before-macro-execution can look like a hang.** A `For...Next` loop using
`getCellByPosition().getValue()` on live `FUNDPRICE`/`FUNDPRICE_HIST` cells took ~5
minutes to process just 2 rows — consistent with LibreOffice forcing a full sheet
recalculation (all ~50 rows × up to 2 live network calls each, executed synchronously)
before the macro body even began. Check Tools → Options → LibreOffice Calc → Formula →
recalculation-on-load/macro settings if a snapshot macro appears to hang.
- **`setDataArray()` bulk copy succeeded where a cell-by-cell `getValue()`/`setValue()`
loop did not** (the loop version produced no visible error but also wrote nothing
usable). The working, confirmed macro uses
`oDest.setDataArray(oSource.getDataArray())` on the full `P3:Q53``R3:S53` range in a
single call rather than iterating cell-by-cell — this is now the standard pattern for
any future static-snapshot needs in this workbook.
---
## 📉 Collabora — Finnhub Removed, STOCKPRICE/STOCKTIME Replaced with FUNDPRICE/FUNDTIME (2026-07-23)
### **Overview**
`STOCKPRICE(A5)` and `STOCKTIME(A5)` (the two Collabora functions calling Finnhub directly
for equity tickers) began returning `#ERROR`. Root cause matched the earlier `FUNDPRICE`
allowlist issue: Finnhub's hostname (`finnhub.io`) had never been added to Collabora's
`net.lok_allow` allowlist in `coolwsd.xml` when the macros were first ported, so every call
was blocked with `access to host denied`, surfaced generically as `#ERROR` by the module's
error handling (same swallowing behavior documented in the ONLYOFFICE troubleshooting
history above).
Rather than add a second allowlist entry for Finnhub, **Finnhub was removed from this
workbook entirely.** Finnhub was only ever used for `STOCKPRICE`/`STOCKTIME` in the first
place because, at the time, it was believed to offer better equity coverage — but it was
already confirmed to have **no historical price data**, which is the original reason
STOCKPROXY was built. Since STOCKPROXY's `/current` endpoint returned valid data for a plain
equity ticker (`SMERY`) with no fund-vs-stock distinction in the API itself, there was no
remaining reason to keep a second price backend or a second allowlist entry.
### **Change Made**
- `STOCKPRICE(ticker)`**removed**. `FUNDPRICE(ticker)` now serves both fund and equity
tickers; STOCKPROXY does not distinguish between them.
- `STOCKTIME(ticker)`**removed**, replaced by a new `FUNDTIME(ticker)` function that
calls the same `/current` endpoint as `FUNDPRICE` and extracts the `resolved_date` field
instead of `price`.
- `FINNHUB_KEY()` config function — **removed**, no longer referenced anywhere in the module.
- New helper `JsonString(sJson, sKey)` added alongside the existing `JsonNumber()` — extracts
a quoted string value from the flat JSON response (`resolved_date` is a string, not a bare
number, so the existing numeric parser couldn't read it).
**Spreadsheet formula changes required (Dashboard tab):**
| Old | New |
|---|---|
| `=STOCKPRICE(A5)` | `=FUNDPRICE(A5)` |
| `=STOCKTIME(A5)` | `=FUNDTIME(A5)` |
### **Known Limitation — `FUNDTIME` Returns a Date, Not a Timestamp**
STOCKPROXY's `/current` response includes `resolved_date` (`YYYY-MM-DD`) but no time-of-day
field. Finnhub's old `STOCKTIME` returned a full Unix timestamp (`t` field) with intraday
resolution, which mattered less for mutual funds (priced once/day) but could matter for
actively-traded equities. This is an accepted tradeoff for now. If time-of-day resolution is
ever needed, STOCKPROXY's Flask app would need a real timestamp field added to the `/current`
response — a small backend change, not yet scoped or requested.
### **Result**
Confirmed working — no `#ERROR` after the swap, both on `SMERY` and equity tickers via
`FUNDPRICE`/`FUNDTIME`. `finnhub\.io` was never actually added to `net.lok_allow` (the
allowlist edit was skipped in favor of removing the Finnhub dependency outright), so no
`coolwsd.xml` changes were required for this fix — only the Basic module and the two
spreadsheet cell formulas changed.
---
## 🤖 Synaplan (AI Document Summarization) — Connector Installed, Backend Not Deployed
The `synaplan_integration` Nextcloud app is installed and enabled, providing a "Summarize with Synaplan" action on files. **This is currently non-functional** — the app is only a thin client/connector; it requires a separate, full Synaplan server stack to be deployed and reachable.
### **What's Missing**
Synaplan's full self-hosted stack (per synaplan.com) consists of:
- PHP backend
- Vue.js frontend
- MariaDB (separate database instance from Nextcloud's)
- Qdrant (vector database, for RAG/semantic search)
- Optionally Ollama (for local AI inference, avoiding external API calls)
None of these components exist anywhere on the KingDezigns network as of 2026-07-18. The Nextcloud app currently points at a default `http://localhost:8000/api/v1/files/upload`, which fails with connection refused since nothing is listening there.
### **Status: Deferred**
Deploying the full Synaplan stack is a separate project (additional containers, database, storage planning, and a decision on local vs. cloud AI model routing) — not a quick config fix. Revisit when ready to scope it properly.
### **Error Reference (for future troubleshooting)**
| Error seen | Cause |
|---|---|
| `Host "localhost" violates local access rules` | Nextcloud's SSRF protection (`allow_local_remote_servers`) blocked the request before `allow_local_remote_servers` was set to `true` |
| `cURL error 7: Failed to connect to localhost port 8000` | Confirmed no Synaplan backend server exists/listens on the expected port — the connector app was installed without its required backend service |
`allow_local_remote_servers` was set to `true` via:
```bash
sudo docker exec -u www-data nextcloud php occ config:system:set allow_local_remote_servers --type=bool --value=true
```
This setting should remain enabled if/when the Synaplan backend is deployed locally.
---
## 🔒 Required Firewall Behavior
### **Inbound to NAS08**
- Allowed from VLAN 1 and VLAN 20 (due to global rules)
- Allowed from VLAN 50 (local VLAN)
- IoT VLAN 30 → DNS allowed (global DNS rule)
- Guest VLAN 40 → DNS allowed (global DNS rule)
- PLEX32 (192.168.150.45) → SSH allowed (local VLAN 50) — required for backup disk stats
### **Outbound from NAS08**
- Subject to VLAN 50's global final drop:
- Cannot initiate to VLANs 1, 10, 20, 30, 40
- Allowed exceptions:
- DNS responses
- Local VLAN 50 traffic
### **Critical Requirement**
- Must remain reachable for DNS from **all VLANs**.
---
## 🔐 Security Measures
- **Fail2Ban enabled**
- Dockerbased service isolation
- VLANlevel containment
- No outbound access to other VLANs (except DNS)
---
## 🗂️ Shared Permissions — Nextcloud + NFS
NAS08 serves files via both **Nextcloud** (Docker, runs as `www-data`) and **NFS** (accessed by `rufusking`).
To allow both to read and write the same files without conflict, a shared group `ncshare` is used with ACLs and NFS squash settings.
### **Group Configuration**
| Item | Value |
|------|-------|
| Group name | `ncshare` |
| GID | `1001` |
| Members | `www-data`, `rufusking` |
Verify with:
```bash
getent group ncshare
```
### **Directory Permissions**
Applied to `/export/kingdezigns-public` (and all subdirectories recursively):
```bash
sudo chown -R www-data:ncshare /export/kingdezigns-public
sudo chmod -R 2770 /export/kingdezigns-public
sudo setfacl -R -m g:ncshare:rwx /export/kingdezigns-public
sudo setfacl -R -d -m g:ncshare:rwx /export/kingdezigns-public
```
- `2770` (setgid) ensures new items inherit the `ncshare` group
- Default ACLs (`-d`) ensure new files/folders created by either Nextcloud or NFS inherit `rwx` for `ncshare`
### **NFS Export Configuration (OMV)**
The `kingdezigns-all` and `kingdezigns-public` NFS shares are configured in **OMV → Services → NFS → Shares** with the following extra options on all client entries:
```
no_subtree_check,insecure,crossmnt,all_squash,anonuid=33,anongid=1001
```
- `all_squash` — maps all NFS client users to the anonymous UID/GID
- `anonuid=33` — maps to `www-data`
- `anongid=1001` — maps to `ncshare`
This ensures files uploaded via NFS land as `www-data:ncshare` regardless of the client user, making them immediately writable by Nextcloud.
> ⚠️ `/etc/exports` is auto-generated by OMV — never edit it directly. All changes must be made through the OMV UI and applied there.
> ⚠️ **Important for PLEX32 backup:** The `all_squash` setting means even `sudo` from PLEX32 cannot create new files on NFS mounts unless the destination directory on NAS08 is owned by `www-data:ncshare` with `2770` permissions. If the PLEX32 backup script fails to copy archives, recheck permissions on the backup directories below.
> ⚠️ **Important for NAS08→NAS16 sync:** The same `all_squash` behavior applies on NAS16's destination. The sync script (`nas08_to_nas16_sync.sh`) self-corrects destination ownership automatically before every run — no manual intervention required.
### **Nextcloud File Scan**
After uploading files via NFS, Nextcloud must be told to index them:
```bash
# Scan all users
sudo docker exec -u www-data nextcloud php occ files:scan --all
# Scan a specific user
sudo docker exec -u www-data nextcloud php occ files:scan RufusKing
sudo docker exec -u www-data nextcloud php occ files:scan ValerieKing
```
Nextcloud users on this system:
| NC Username | Display Name |
|-------------|--------------|
| RufusKing | Rufus King |
| ValerieKing | Valerie King |
### **Troubleshooting**
If a file uploaded before the NFS squash fix still shows `rufusking:rufusking` ownership, fix it manually:
```bash
sudo chown www-data:ncshare "/path/to/file"
```
Then rescan in Nextcloud. Going forward, all new NFS uploads will land with correct ownership automatically.
---
## 💾 PLEX32 Backup — NAS08 Side
NAS08 receives automated config backups from PLEX32 every 3 days via NFS.
### **Backup Directories**
```
/export/kingdezigns-all/Docker/plex/config/backups/plex/
/export/kingdezigns-all/Docker/plex/config/backups/tautulli/
```
### **Required Permissions**
All backup directories must be owned by `www-data:ncshare` with `2770` to allow PLEX32 to write via NFS all_squash:
```bash
sudo chown -R www-data:ncshare /export/kingdezigns-all/Docker/plex/config
sudo chmod -R 2770 /export/kingdezigns-all/Docker/plex/config
```
### **Verify backup directories exist and have correct ownership**
```bash
ls -la /export/kingdezigns-all/Docker/plex/config/
ls -la /export/kingdezigns-all/Docker/plex/config/backups/
```
Expected output: `www-data:ncshare` ownership, `drwxrws---` permissions on all directories.
### **SSH Access from PLEX32**
PLEX32 has an SSH key installed on NAS08 (`rufusking` account) — used by the backup script to fetch accurate disk stats via `df`. This is read-only and harmless.
---
## 🛠️ SSD Hardening & Maintenance
### **TRIM**
- TRIM support confirmed on all drives (`DISC-GRAN: 4K`, `DISC-MAX: 4G`)
- systemd `fstrim.timer` **disabled** — replaced by OMV Scheduled Job
- Weekly TRIM script: `/usr/scripts/omv/fstrim-report.sh`
- Scheduled: **Wednesday at 1:00 AM** via OMV Scheduled Jobs
- Script runs `fstrim -av`, gathers SMART health data, sends HTML email report, saves dated log to `/var/log/fstrim/`
- Log retention: 90 days
### **noatime**
- All filesystems mounted with `noatime` — confirmed via `findmnt`
- Managed automatically by the `openmediavault-flashmemory` plugin
- No manual configuration required
### **openmediavault-flashmemory Plugin**
- Installed and active
- Automatically handles: `noatime`, tmpfs for `/var/log` and `/tmp`, swap disabled
- Significantly reduces unnecessary SSD write amplification
### **SMART Monitoring**
- Monitored weekly as part of the TRIM report script
- Drive-specific attribute mapping:
| Drive | Wear Attribute | Reallocated Attribute | Notes |
|-------|---------------|----------------------|-------|
| PNY CS900 (/dev/sda) | `SSD_Life_Left` (attr 231) | `Bad_Blk_Ct_Lat/Erl` (attr 170, first value) | No pending/uncorrectable attrs |
| Crucial BX500 (/dev/sdbsde) | `Percent_Lifetime_Remain` (attr 202) | `Reallocate_NAND_Blk_Cnt` (attr 5) | Uses `Reported_Uncorrect` for uncorrectable |
---
## 📅 Scheduled Jobs (OMV)
| Schedule | Script | Tag |
|----------|--------|-----|
| Every 3 days at 2:00 AM | `/usr/scripts/omv/nas08-backup.sh` | NAS08 Backup |
| Daily at 12:00 AM | `/usr/scripts/zfs/nas08_zfs_report.sh` | ZFS Status Report |
| Daily at 7:00 AM | `/usr/scripts/omv/nextcloud_update_check.sh` | Nextcloud Update Check |
| Sunday at 3:00 AM | `/usr/scripts/zfs/nas08_zfs_scrub.sh` | ZFS Scrub |
| Wednesday at 1:00 AM | `/usr/scripts/omv/fstrim-report.sh` | TRIM Report |
| Disabled (manual) | `/usr/scripts/nas/plex32_reboot.sh` | PLEX32 Remote Reboot |
---
## 🔄 Nextcloud Update Procedure
**Never use the Nextcloud browser UI to trigger app updates.** Apache workers handle browser-initiated updates and can segfault mid-update, leaving Nextcloud stuck in maintenance mode with cron also blocked — a self-reinforcing deadlock that requires manual `occ` intervention to recover.
All Nextcloud updates must be performed via `occ` on the command line.
### **Daily Automated Check**
- **Script:** `/usr/scripts/omv/nextcloud_update_check.sh`
- **Schedule:** Daily at 7:00 AM via OMV Scheduled Jobs
- **SMTP password:** `/etc/nextcloud-smtp-pass` (chmod 600)
- **Log:** `/var/log/nextcloud-updates/` (90 day retention)
- **Email:** HTML report sent daily — green (up to date) / amber (updates available) / red (maintenance mode or DB upgrade needed)
- The email includes a ready-to-paste command block for any action required
### **Manual Update Commands**
Run these on NAS08 whenever the email reports updates available:
```bash
# Update all apps (instead of clicking Update in browser)
sudo docker exec -u www-data nextcloud php occ app:update --all
# After docker compose pull + up (Nextcloud container version update)
sudo docker exec -u www-data nextcloud php occ upgrade --no-interaction
sudo docker exec -u www-data nextcloud php occ app:update --all
# Verify clean state after any update
sudo docker exec -u www-data nextcloud php occ status
```
### **Recovery — If Stuck in Maintenance Mode**
```bash
sudo docker exec -u www-data nextcloud php occ maintenance:mode --off
sudo docker exec -u www-data nextcloud php occ upgrade --no-interaction
sudo docker exec -u www-data nextcloud php occ app:update --all
sudo docker exec -u www-data nextcloud php occ maintenance:repair
```
---
## 🔄 Nextcloud Update Check Script — Core Version Detection Bug Fix (2026-07-25)
### **Overview**
`nextcloud_update_check.sh` (see Scheduled Jobs above) was reporting **"✅ Nextcloud is
fully up to date"** by email while Nextcloud 34.0.2 was actually available — a genuine
false negative, not a display glitch. Root-caused, fixed, and verified working the same
day.
### **Root Cause**
The script only ever ran `occ app:update --all --showonly`, which checks **app** updates
only. It never checked for a Nextcloud **core/server** version update — that requires the
separate `occ update:check` command, which queries the updater channel
(updater.nextcloud.com), not the app store. Since 34.0.2 is a core release, not an app
release, the script had no code path capable of seeing it at all — it wasn't a parsing bug,
it was checking the wrong thing entirely.
### **Fix Applied to the Script**
- Added a core-update check via `occ update:check`, parsed for `"Nextcloud X.Y.Z is
available"`.
- `NEEDS_ACTION` and the email's status color/badge now factor in a core update, not just
app updates.
- Added a dedicated "🆙 Core Update Available" block in the email showing current → new
version.
- Fixed a second, independent bug found during testing: the emailed copy-paste command
block used `"...\n\n"` inside plain double-quoted bash strings to build multi-line
output. Plain double quotes do **not** interpret `\n` as a newline in bash — they insert
the two literal characters `\` and `n`. Every command in the email was arriving as one
unreadable line with literal `\n` text in it. Fixed by switching to ANSI-C quoting
(`$'...'`) for all multi-line command block construction.
- Removed one leftover unused variable (`CORE_ROW`) from the original script.
### **Command Block Path Bug — Found During Live Testing**
The first fixed version of the script emailed a generic guessed update command
(`cd ~/docker/nextcloud && docker compose pull && docker compose up -d`). Running it
live surfaced two additional real environment facts that had to be baked into the script:
1. **Wrong directory.** NAS08's actual Nextcloud Docker Compose project lives at
`/kingdezignsnas/Docker/Compose/nextcloud/`, not `~/docker/nextcloud/`. Confirmed via:
```bash
docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'
```
2. **Root-only directory + non-default filenames.** The directory is `drwx------ root
root` — `rufusking` cannot `cd` into it without `sudo`. OMV's Compose plugin also does
not name the file `docker-compose.yml`; it uses `nextcloud.yml` +
`compose.override.yml`, which `docker compose` does not auto-discover without explicit
`-f` flags.
**Corrected command now emitted by the script:**
```bash
sudo bash -c "cd /kingdezignsnas/Docker/Compose/nextcloud && docker compose -f nextcloud.yml -f compose.override.yml pull && docker compose -f nextcloud.yml -f compose.override.yml up -d"
```
### **False "No Upgrade Required" — Race Condition, Not a Bug**
After running the corrected command, `occ upgrade --no-interaction` and `occ status`
immediately afterward both still reported the **old** version (34.0.1.2), even though
`docker compose pull` had genuinely fetched the new 34.0.2 image and the container had
restarted. This looked like the update silently failing and was re-attempted twice with
identical results before being root-caused.
**Actual cause:** the official `nextcloud` image's entrypoint script performs the
version-upgrade itself automatically on container start — copying in the new application
code and internally running `occ upgrade` — it does **not** require (or expect) the admin
to run `occ upgrade` manually. Checking `occ status` immediately after `up -d` races the
container's own startup: the check fires before the entrypoint's internal upgrade has
finished, so it reports the pre-upgrade version. Confirmed via `docker logs nextcloud`,
which showed `Upgrading nextcloud from 34.0.1.2 ...` → `Update successful`, completing
about 30 seconds after container start — after which `occ status` correctly showed
`34.0.2`.
**Fix:** the script's emailed command block now includes a `sleep 30` between `up -d` and
the confirming `occ status` call, so the copy-pasted commands don't produce the same false
negative.
### **Verification Method**
The corrected script was validated with a full mock test harness — fake `docker`/`sudo`
binaries standing in for the real container, run under `bash -u` (aborts on any unset
variable) — exercising all three code paths: core+app updates available, container not
running, and fully up to date. All three produced correct email content with real newlines
in the command block (verified via direct newline-count check, not just visual
inspection). Confirmed working against the live system same day — `occ status` returned
`versionstring: 34.0.2` after following the corrected emailed commands, and a subsequent
run of the script correctly reported "up to date."
### **Current Script Behavior (as of 2026-07-25)**
| Check | Command Used | Detects |
|---|---|---|
| Core/server version | `occ update:check` | Nextcloud core releases (e.g. 34.0.1 → 34.0.2) |
| App versions | `occ app:update --all --showonly` | Individual app updates (e.g. `files_lock`, `bookmarks`) |
| Maintenance mode / DB upgrade flag | `occ status` | Stuck maintenance mode, pending DB migration |
---
## 🧠 Summary for AI Systems
- NAS08 = **DNS + application server** in VLAN 50.
- Provides Pihole DNS to all VLANs.
- Hosts Nextcloud, Vaultwarden, and nginx.
- Inbound allowed from VLAN 1, VLAN 20, and local VLAN 50.
- Outbound blocked to all VLANs except local + DNS responses.
- Critical infrastructure component for networkwide DNS and storage.
- SSD hardening active: TRIM (weekly Wednesday 1AM), noatime (flashmemory plugin), SMART monitoring via weekly HTML email report.
- Five drives: 1x PNY CS900 500GB (OS), 4x Crucial BX500 1TB (data).
- **Shared permissions:** `ncshare` group (GID 1001) shared by `www-data` and `rufusking` — allows both Nextcloud and NFS to read/write the same files.
- NFS exports use `all_squash,anonuid=33,anongid=1001` — all NFS uploads land as `www-data:ncshare` automatically.
- Default ACLs on `/export/kingdezigns-public` ensure all new files/folders inherit `ncshare` group permissions.
- After NFS uploads, run `occ files:scan` to index new files in Nextcloud.
- Nextcloud container name: `nextcloud` — cron container: `nextcloud_cron` — DB: `nextcloud_db`.
- **Never update Nextcloud apps via the browser UI** — Apache segfaults mid-update leave Nextcloud stuck in maintenance mode. Always use `occ app:update --all` from the command line.
- **Nextcloud update check script:** `/usr/scripts/omv/nextcloud_update_check.sh` — runs daily at 7AM, emails HTML report with ready-to-paste update commands. SMTP password at `/etc/nextcloud-smtp-pass`.
- **Nextcloud update check script — core-update false negative fixed (2026-07-25):** the script previously only checked `occ app:update` (app updates), never `occ update:check` (core/server updates), so a core release like 34.0.2 was invisible and the email falsely reported "up to date." Fixed by adding a core-update check, fixing a `\n`-in-double-quotes bug that made emailed commands unreadable, and correcting the emailed update commands to use the real Compose path (`/kingdezignsnas/Docker/Compose/nextcloud/`, root-only, requires `sudo bash -c` + explicit `-f nextcloud.yml -f compose.override.yml` flags — not the default `docker-compose.yml`/`~/docker/nextcloud` assumed originally). Also documented: the official Nextcloud image auto-upgrades on container restart via its own entrypoint — running `occ upgrade` or checking `occ status` immediately after `docker compose up -d` can race that internal process and falsely show the old version for ~30 seconds. The emailed command block now includes a `sleep 30` before the confirming status check. See dedicated section above for full troubleshooting detail and the mock-harness verification method used to confirm the fix.
- **Maintenance mode recovery:** `occ maintenance:mode --off` → `occ upgrade --no-interaction` → `occ app:update --all` → `occ maintenance:repair`.
- **PLEX32 backup destination:** `/export/kingdezigns-all/Docker/plex/config/backups/` — plex and tautulli subdirs receive tar archives every 3 days. Must be `www-data:ncshare 2770` or writes will fail.
- PLEX32 has SSH key installed on NAS08 rufusking account — used for backup disk stats only.
- **NAS08 root SSH key installed on PLEX32** — required for `plex32_reboot.sh` remote reboot script.
- **Remote reboot script:** `/usr/scripts/nas/plex32_reboot.sh` — SSHs into PLEX32, stops containers, reboots, waits for recovery, starts containers, confirms Plex on port 32400 via curl. Registered as disabled OMV Scheduled Job — enable and run manually when needed. Logs to `/var/log/plex32-reboot/`.
- **NAS08→NAS16 sync destination permissions are self-healing** — `nas08_to_nas16_sync.sh` on NAS16 automatically checks and corrects `www-data:ncshare` ownership on `/export/kingdezignsnas-16/Public` before every run. No manual chown/chmod/setfacl needed on NAS16.
- NFS `all_squash` on NAS08 exports is the root cause of rsync permission errors on NAS16 — the sync script handles this transparently via `--no-perms`, `--omit-dir-times`, and the permission precheck.
- **ONLYOFFICE (2026-07-18):** Nextcloud document editing runs via ONLYOFFICE Document Server (Community Edition, free) — Docker container `onlyoffice`, image `onlyoffice/documentserver:latest`, port 9980, public URL `https://office.kingdezigns.com/`. Only the `onlyoffice` Nextcloud app is enabled (eurooffice/officeonline/office disabled — never re-enable more than one Office app simultaneously). Requires `ALLOW_PRIVATE_IP_ADDRESS=true` env var, JWT auth enabled with a shared secret between Nextcloud app config and container `JWT_SECRET`, and a Pi-hole local DNS record for `cloud.kingdezigns.com` → `192.168.150.30` to avoid a NAT hairpin failure. See dedicated section above for full troubleshooting history.
- **DNS fix (2026-07-18):** `/etc/systemd/resolved.conf` had a stray global `DNS=1.1.1.1 8.8.8.8` override that merged with the correct per-interface Pi-hole DNS, causing duplicate/incorrect resolution for internal hostnames. Removed — only `FallbackDNS=9.9.9.9` remains as a last-resort. Docker containers require a restart to pick up host DNS changes.
- **Synaplan (2026-07-18):** `synaplan_integration` Nextcloud app is installed but non-functional — it is only a connector; the required Synaplan backend server stack (PHP, MariaDB, Qdrant, optional Ollama) has not been deployed. Deploying it is a separate future project. `allow_local_remote_servers` was set to `true` system-wide to unblock local-network AI backend calls in general (needed for Synaplan and similar future integrations).
- **STOCKPROXY (2026-07-21):** Self-hosted Flask API, Docker container `stockproxy`, own OMV-style Compose project at `~/docker/stockproxy/`, port 5005, public URL `https://stocks.kingdezigns.com/` (proxied via NPM on HAS). Supplies current + historical mutual fund prices to ONLYOFFICE via Yahoo Finance (server-side scrape, bypasses browser CORS block) — Finnhub was evaluated first but has no mutual fund NAV coverage and was dropped from this service. Current-price cache: 15 min in-memory. Historical-price cache: permanent, SQLite at `/opt/stockproxy/data/cache.db`. All endpoints require a shared-secret `key` param (Vaultwarden). Built to support the "HSA Investiments Choices" tab in `Rufus Retirement Account Tracking 2026.xlsx`. See dedicated section above for full config, endpoints, and known limitations.
- **RANK.EQ Err:522 circular reference fix (2026-07-23, working):** The "HSA
Investiments Choices" tab's fund ranking (`RANK.EQ`) broke with a circular-reference
error (Err:522) on any sheet edit — but only after loading correctly — because it
depended on ~50 cells fed by live `FUNDPRICE`/`FUNDPRICE_HIST` network calls, and an
incremental recalc could catch those async cells mid-flight. Fixed by adding a
button-triggered LibreOffice Basic macro (`CopyStockValues`) that snapshots the live
`P`/`Q` price columns into static value-only columns `R`/`S` via
`oDest.setDataArray(oSource.getDataArray())`; percent-change (`T`/`U`) and
`RANK.EQ` were rebuilt to read from the static columns instead of the live ones. `P`/`Q`
remain live and unaffected — only the ranking chain was decoupled. See dedicated section
above for full root-cause analysis, macro code, and diagnostic dead ends (notably:
`ActiveSheet` must be used instead of `Sheets.getByIndex(0)` for multi-tab workbooks, and
a cell-by-cell `getValue()`/`setValue()` loop silently failed where a bulk
`setDataArray()` copy succeeded).
- **Collabora Finnhub removal — STOCKPRICE/STOCKTIME replaced with FUNDPRICE/FUNDTIME
(2026-07-23, working):** `STOCKPRICE`/`STOCKTIME` errored because Finnhub's hostname was
never added to Collabora's `net.lok_allow` allowlist. Rather than add it, Finnhub was
dropped entirely — `FUNDPRICE` now also serves equity tickers (STOCKPROXY doesn't
distinguish fund vs. stock), and a new `FUNDTIME` function reads STOCKPROXY's
`resolved_date` field in place of Finnhub's timestamp. Note: `FUNDTIME` returns a date
only, not time-of-day — a regression from Finnhub's old intraday timestamp, accepted as a
tradeoff for now. `FINNHUB_KEY()` config and both old functions removed from
`StockFunctions.bas`; new `JsonString()` helper added for parsing quoted JSON string
fields. See dedicated section above.
- **Collabora stock/fund price macro integration (2026-07-22, working):** Ported the ONLYOFFICE `FUNDPRICE`/`FUNDPRICE_HIST`/`STOCKPRICE`/`STOCKTIME` custom functions to a LibreOffice Basic module (`StockFunctions.bas`) calling the same STOCKPROXY/Finnhub backends — no backend changes needed. Required: (1) Collabora test copy saved as `.ods` not `.xlsx` — Basic modules don't reliably persist in `.xlsx`; (2) `enable_macros_execution=true` in `coolwsd.xml` (disabled by default post-CVE-2025-24796); (3) added `stocks\.kingdezigns\.com` to the `net.lok_allow` host allowlist in `coolwsd.xml` — this allowlist matches literal hostname/IP-text regex patterns, not DNS-resolved IPs, so the hostname needed its own explicit entry even though it resolves to an already-allowed private IP. Macro edits must be done in desktop LibreOffice (Collabora's browser UI has no working Basic IDE) then re-uploaded. Also fixed during this work: the `stocks.kingdezigns.com` Pi-hole local DNS override existed but was not actually resolving (returning WAN IP) — fixed with `docker restart pihole` (v6's `reloaddns` errored, `restartdns` doesn't exist in v6). See dedicated section above for full troubleshooting detail, exact XML edits, and open follow-up items.
---
# ✔️ End of File