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

61 KiB
Raw Blame History

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):

  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=truerequired. 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

# 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.

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

# 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 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.com192.168.150.30. During this session that record was found to not actually be resolving correctlydig @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 fixreloaddns 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):

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:Q53R3: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:

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:

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 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:

# 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.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:

# 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_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:
    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 rootrufusking 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:

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 --offocc upgrade --no-interactionocc app:update --allocc 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-healingnas08_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.com192.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