infrastructure/server_nas08.md

1019 lines
61 KiB
Markdown
Raw Normal View History

2026-07-26 22:12:38 -04:00
# 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