61 KiB
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 multi‑service application server providing DNS, storage, media hosting, and self‑hosted applications for the KingDezigns network.
It is the Pi‑hole DNS provider for all VLANs and also hosts several Docker‑based 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. Seeserver_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 Pi‑hole (192.168.150.35) as its only DNS server via DHCP.
⚠️ Known issue, fixed 2026-07-18:
/etc/systemd/resolved.confpreviously had a manually-set global override:[Resolve] DNS=1.1.1.1 8.8.8.8 FallbackDNS=9.9.9.9Because
resolv.conf modeisuplink, this globalDNS=line merged with the correct per-interface Pi‑hole entry, causing/etc/resolv.confto 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, leavingFallbackDNS=9.9.9.9in place as a last-resort fallback only. Verify withresolvectl status— theGlobalblock should show noDNS Servers:line, andLink 2 (eth0)should show only192.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.confat container start.
📦 Primary Functions
Core Services
- Pi‑hole 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 Office–style 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):
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'sonlyofficeapp — this does not appear in the standardnextcloud.logPHP-level log; it only shows in the Apache/container stdout log (docker logs nextcloud) and innextcloud.logif 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
# 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:
- Four Office apps were installed simultaneously (
onlyoffice,eurooffice,officeonline,office), causing Nextcloud to route file-open actions unpredictably. Resolved by disabling all butonlyoffice. - Host DNS split-brain — a stray global
DNS=1.1.1.1 8.8.8.8in/etc/systemd/resolved.confcausedcloud.kingdezigns.comto 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. - 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. - 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 ofnextcloud.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.
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:
Dockerfilebuilds onpython:3.12-slim, installsflask/requests/gunicorn, runs viagunicorn --bind 0.0.0.0:5005 --workers 2 --timeout 30 app:app(production WSGI server, not Flask's dev server).- Data volume
/opt/stockproxy/dataon the host (owned byrufusking, created viasudo mkdir+chownback to the normal user — same permission pattern used elsewhere on NAS08) holds the permanent SQLite historical-price cache atcache.db, so it survives container rebuilds/restarts. - No
FINNHUB_API_KEYvariable — 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
# 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-Agentpattern at some point — not a documented/supported API. Same risk category as the PLEX32docker pullintermittent-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/Aacross 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()/asyncin LibreOffice Basic. HTTP GET is implemented via thecom.sun.star.ucb.SimpleFileAccess+com.sun.star.io.TextInputStreamUNO 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 parallelJsonString()helper (added 2026-07-23, see Finnhub removal section below) extracts quoted string values the same way, forresolved_date. - A minimal
EncodeUrl()percent-encoder handles ticker symbols andyyyy-mm-dddate 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/currentcache 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 Functions 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:
<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:
<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:
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:
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 Functions). 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:
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_executionwas previously hardened tofalseby default following CVE-2025-24796 (a macro-based remote code execution vulnerability affecting Collabora's jailed macro execution model).macro_security_levelwas deliberately left at1(Medium, prompts before running) rather than0(always allow, no prompt) to retain a manual confirmation step.- The
net.lok_allowallowlist exists specifically to limit which hosts a jailed macro can reach, even with execution enabled — onlystocks.kingdezigns.comwas added, not a broader wildcard.
Open / Follow-up Items
- Recreate the ~50-fund table and ranking table (
RANK.EQ++ROW()/1000000tie-breaker) in the Collabora.odscopy — 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):
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. UseoDoc.CurrentController.ActiveSheetinstead to always target the tab currently in view.- Recalculation-before-macro-execution can look like a hang. A
For...Nextloop usinggetCellByPosition().getValue()on liveFUNDPRICE/FUNDPRICE_HISTcells 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-cellgetValue()/setValue()loop did not (the loop version produced no visible error but also wrote nothing usable). The working, confirmed macro usesoDest.setDataArray(oSource.getDataArray())on the fullP3:Q53→R3:S53range 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 newFUNDTIME(ticker)function that calls the same/currentendpoint asFUNDPRICEand extracts theresolved_datefield instead ofprice.FINNHUB_KEY()config function — removed, no longer referenced anywhere in the module.- New helper
JsonString(sJson, sKey)added alongside the existingJsonNumber()— extracts a quoted string value from the flat JSON response (resolved_dateis 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:
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
- Docker‑based service isolation
- VLAN‑level 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:
getent group ncshare
Directory Permissions
Applied to /export/kingdezigns-public (and all subdirectories recursively):
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 thencsharegroup- Default ACLs (
-d) ensure new files/folders created by either Nextcloud or NFS inheritrwxforncshare
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/GIDanonuid=33— maps towww-dataanongid=1001— maps toncshare
This ensures files uploaded via NFS land as www-data:ncshare regardless of the client user, making them immediately writable by Nextcloud.
⚠️
/etc/exportsis 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_squashsetting means evensudofrom PLEX32 cannot create new files on NFS mounts unless the destination directory on NAS08 is owned bywww-data:ncsharewith2770permissions. If the PLEX32 backup script fails to copy archives, recheck permissions on the backup directories below.
⚠️ Important for NAS08→NAS16 sync: The same
all_squashbehavior 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:
# 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:
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:
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
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.timerdisabled — 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 viafindmnt - Managed automatically by the
openmediavault-flashmemoryplugin - No manual configuration required
openmediavault-flashmemory Plugin
- Installed and active
- Automatically handles:
noatime, tmpfs for/var/logand/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/sdb–sde) | 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:
# 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
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_ACTIONand 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\nas a newline in bash — they insert the two literal characters\andn. Every command in the email was arriving as one unreadable line with literal\ntext 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:
- Wrong directory. NAS08's actual Nextcloud Docker Compose project lives at
/kingdezignsnas/Docker/Compose/nextcloud/, not~/docker/nextcloud/. Confirmed via:docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' - Root-only directory + non-default filenames. The directory is
drwx------ root root—rufuskingcannotcdinto it withoutsudo. OMV's Compose plugin also does not name the filedocker-compose.yml; it usesnextcloud.yml+compose.override.yml, whichdocker composedoes not auto-discover without explicit-fflags.
Corrected command now emitted by the script:
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 Pi‑hole 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 network‑wide 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:
ncsharegroup (GID 1001) shared bywww-dataandrufusking— allows both Nextcloud and NFS to read/write the same files. - NFS exports use
all_squash,anonuid=33,anongid=1001— all NFS uploads land aswww-data:ncshareautomatically. - Default ACLs on
/export/kingdezigns-publicensure all new files/folders inheritncsharegroup permissions. - After NFS uploads, run
occ files:scanto 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 --allfrom 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), neverocc 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, requiressudo bash -c+ explicit-f nextcloud.yml -f compose.override.ymlflags — not the defaultdocker-compose.yml/~/docker/nextcloudassumed originally). Also documented: the official Nextcloud image auto-upgrades on container restart via its own entrypoint — runningocc upgradeor checkingocc statusimmediately afterdocker compose up -dcan race that internal process and falsely show the old version for ~30 seconds. The emailed command block now includes asleep 30before 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 bewww-data:ncshare 2770or 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.shremote 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.shon NAS16 automatically checks and correctswww-data:ncshareownership on/export/kingdezignsnas-16/Publicbefore every run. No manual chown/chmod/setfacl needed on NAS16. - NFS
all_squashon 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, imageonlyoffice/documentserver:latest, port 9980, public URLhttps://office.kingdezigns.com/. Only theonlyofficeNextcloud app is enabled (eurooffice/officeonline/office disabled — never re-enable more than one Office app simultaneously). RequiresALLOW_PRIVATE_IP_ADDRESS=trueenv var, JWT auth enabled with a shared secret between Nextcloud app config and containerJWT_SECRET, and a Pi-hole local DNS record forcloud.kingdezigns.com→192.168.150.30to avoid a NAT hairpin failure. See dedicated section above for full troubleshooting history. - DNS fix (2026-07-18):
/etc/systemd/resolved.confhad a stray globalDNS=1.1.1.1 8.8.8.8override that merged with the correct per-interface Pi-hole DNS, causing duplicate/incorrect resolution for internal hostnames. Removed — onlyFallbackDNS=9.9.9.9remains as a last-resort. Docker containers require a restart to pick up host DNS changes. - Synaplan (2026-07-18):
synaplan_integrationNextcloud 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_serverswas set totruesystem-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 URLhttps://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-secretkeyparam (Vaultwarden). Built to support the "HSA Investiments Choices" tab inRufus 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 liveFUNDPRICE/FUNDPRICE_HISTnetwork 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 liveP/Qprice columns into static value-only columnsR/SviaoDest.setDataArray(oSource.getDataArray()); percent-change (T/U) andRANK.EQwere rebuilt to read from the static columns instead of the live ones.P/Qremain 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:ActiveSheetmust be used instead ofSheets.getByIndex(0)for multi-tab workbooks, and a cell-by-cellgetValue()/setValue()loop silently failed where a bulksetDataArray()copy succeeded). - Collabora Finnhub removal — STOCKPRICE/STOCKTIME replaced with FUNDPRICE/FUNDTIME
(2026-07-23, working):
STOCKPRICE/STOCKTIMEerrored because Finnhub's hostname was never added to Collabora'snet.lok_allowallowlist. Rather than add it, Finnhub was dropped entirely —FUNDPRICEnow also serves equity tickers (STOCKPROXY doesn't distinguish fund vs. stock), and a newFUNDTIMEfunction reads STOCKPROXY'sresolved_datefield in place of Finnhub's timestamp. Note:FUNDTIMEreturns 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 fromStockFunctions.bas; newJsonString()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/STOCKTIMEcustom 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.odsnot.xlsx— Basic modules don't reliably persist in.xlsx; (2)enable_macros_execution=trueincoolwsd.xml(disabled by default post-CVE-2025-24796); (3) addedstocks\.kingdezigns\.comto thenet.lok_allowhost allowlist incoolwsd.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: thestocks.kingdezigns.comPi-hole local DNS override existed but was not actually resolving (returning WAN IP) — fixed withdocker restart pihole(v6'sreloaddnserrored,restartdnsdoesn't exist in v6). See dedicated section above for full troubleshooting detail, exact XML edits, and open follow-up items.