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