1019 lines
61 KiB
Markdown
1019 lines
61 KiB
Markdown
|
|
# 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=<secret — stored in Vaultwarden, not in plaintext docs>
|
|||
|
|
- ALLOW_PRIVATE_IP_ADDRESS=true
|
|||
|
|
networks:
|
|||
|
|
- nextcloud_net
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key environment variables:**
|
|||
|
|
- `ALLOW_PRIVATE_IP_ADDRESS=true` — **required**. Without this, the Document Server's built-in SSRF protection rejects callback requests to Nextcloud's private LAN IP/hostname, causing "Error while downloading the document file to be converted."
|
|||
|
|
- `JWT_ENABLED=true` + `JWT_SECRET=...` — requires every request between Nextcloud and the Document Server to be signed. Without a matching secret on both sides, requests fail with a silent `{"message":"Access denied"}` from Nextcloud's `onlyoffice` app — this does **not** appear in the standard `nextcloud.log` PHP-level log; it only shows in the Apache/container stdout log (`docker logs nextcloud`) and in `nextcloud.log` if you grep specifically for `"app":"onlyoffice"`.
|
|||
|
|
|
|||
|
|
### **Pi-hole Local DNS Requirement**
|
|||
|
|
The ONLYOFFICE Document Server container must reach Nextcloud at its **public hostname** (`cloud.kingdezigns.com`) to download/upload files, because that's the `StorageUrl` Nextcloud is configured with (`overwritehost` = `cloud.kingdezigns.com`). Without a local DNS override, this hostname resolves to the WAN IP, causing a NAT hairpin request that typically fails.
|
|||
|
|
|
|||
|
|
**Required Pi-hole Local DNS Record:**
|
|||
|
|
| Domain | IP |
|
|||
|
|
|---|---|
|
|||
|
|
| `cloud.kingdezigns.com` | `192.168.150.30` |
|
|||
|
|
|
|||
|
|
Configured under Pi-hole Admin → Local DNS → DNS Records. This also improves performance for all other internal clients accessing Nextcloud by its public domain name.
|
|||
|
|
|
|||
|
|
### **Verification Commands**
|
|||
|
|
```bash
|
|||
|
|
# Confirm only onlyoffice app is enabled (not eurooffice/officeonline/office)
|
|||
|
|
sudo docker exec -u www-data nextcloud php occ app:list | grep -i office
|
|||
|
|
|
|||
|
|
# Confirm JWT secret is set and matches on both sides
|
|||
|
|
sudo docker exec -u www-data nextcloud php occ config:app:get onlyoffice jwt_secret
|
|||
|
|
sudo docker inspect onlyoffice --format '{{json .Config.Env}}' | tr ',' '\n' | grep JWT
|
|||
|
|
|
|||
|
|
# Confirm container can resolve and reach Nextcloud internally
|
|||
|
|
sudo docker exec onlyoffice getent hosts cloud.kingdezigns.com # should return ONLY 192.168.150.30
|
|||
|
|
sudo docker exec onlyoffice curl -sk -o /dev/null -w "%{http_code}\n" https://cloud.kingdezigns.com/
|
|||
|
|
|
|||
|
|
# Watch live Document Server logs during a test document open
|
|||
|
|
sudo docker logs -f onlyoffice --tail 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **Troubleshooting History (2026-07-17/18)**
|
|||
|
|
Office editing broke silently after previously working. Root causes, in the order they were found and fixed:
|
|||
|
|
1. **Four Office apps were installed simultaneously** (`onlyoffice`, `eurooffice`, `officeonline`, `office`), causing Nextcloud to route file-open actions unpredictably. Resolved by disabling all but `onlyoffice`.
|
|||
|
|
2. **Host DNS split-brain** — a stray global `DNS=1.1.1.1 8.8.8.8` in `/etc/systemd/resolved.conf` caused `cloud.kingdezigns.com` to resolve to both the correct internal IP and the public WAN IP, producing a NAT hairpin failure. Fixed by removing the global override (see DNS section above) and adding a Pi-hole local DNS record.
|
|||
|
|
3. **SSRF private-IP block** — the Document Server refused to call back to Nextcloud's private IP by default. Fixed with `ALLOW_PRIVATE_IP_ADDRESS=true`.
|
|||
|
|
4. **JWT mismatch** — Nextcloud had a JWT secret configured while the container had `JWT_ENABLED=false`, causing every Document Server callback to be silently rejected with `{"message":"Access denied"}` — invisible in normal logs, only visible via Apache-level container stdout log or a targeted grep of `nextcloud.log`. Fixed by aligning JWT config on both sides (currently: enabled, with a shared secret).
|
|||
|
|
|
|||
|
|
**Diagnostic lesson learned:** Nextcloud's PHP-level `nextcloud.log` does **not** capture ONLYOFFICE app-level "Access denied" rejections by default in a way that's easy to spot — always check `docker logs nextcloud` (Apache access log) directly for the actual HTTP status code of the failing request, then grep `nextcloud.log` for the specific `app":"onlyoffice"` entries at that timestamp.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📈 STOCKPROXY — Self-Hosted Stock/Fund Price API
|
|||
|
|
|
|||
|
|
### **Overview**
|
|||
|
|
As of 2026-07-21, NAS08 runs **STOCKPROXY**, a small self-hosted Flask API that supplies
|
|||
|
|
current and historical stock/mutual-fund prices to ONLYOFFICE spreadsheets. It was built
|
|||
|
|
because ONLYOFFICE has no equivalent to Excel's `STOCKHISTORY()`/linked stock data types,
|
|||
|
|
and neither of the obvious alternatives worked on their own:
|
|||
|
|
|
|||
|
|
- **Finnhub** (free tier) has no mutual fund NAV coverage at all — confirmed via direct
|
|||
|
|
testing, returns `{"error":"no data"}` for every mutual fund ticker tried (MKDVX, FIFRX,
|
|||
|
|
etc.). Finnhub is still used directly (not through this proxy) for regular equities via
|
|||
|
|
separate ONLYOFFICE custom functions on the "Dashboard" tab.
|
|||
|
|
- **Yahoo Finance** has good mutual fund coverage (current + historical) but explicitly
|
|||
|
|
blocks direct browser-based (CORS) requests — confirmed by Yahoo Finance library
|
|||
|
|
maintainers themselves ("we will not help you bypass" the block).
|
|||
|
|
|
|||
|
|
STOCKPROXY solves this by calling Yahoo Finance **server-to-server** (no CORS restriction
|
|||
|
|
applies between servers), then re-serving the result to the browser with permissive CORS
|
|||
|
|
headers so an ONLYOFFICE custom function running client-side can call it directly.
|
|||
|
|
|
|||
|
|
Used by the "HSA Investiments Choices" tab of `Rufus Retirement Account Tracking 2026.xlsx`
|
|||
|
|
to track ~50 mutual funds. See that workbook's own documentation for the spreadsheet-side
|
|||
|
|
custom functions (`FUNDPRICE`, `FUNDPRICE_HIST`) and cell layout.
|
|||
|
|
|
|||
|
|
### **Current Configuration**
|
|||
|
|
| Component | Value |
|
|||
|
|
|---|---|
|
|||
|
|
| Container name | `stockproxy` (compose service name) |
|
|||
|
|
| Container host | NAS08, standalone Docker Compose project at `~/docker/stockproxy/` |
|
|||
|
|
| Internal port | 5005 (host and container match) |
|
|||
|
|
| Public URL | `https://stocks.kingdezigns.com/` (proxied via NPM on HAS — see `server_homeassistant.md` Proxy Hosts table) |
|
|||
|
|
| Data source | Yahoo Finance unofficial chart API (`query1.finance.yahoo.com/v8/finance/chart/{ticker}`), scraped server-side with a browser-like `User-Agent` header — no API key required |
|
|||
|
|
| Auth | Shared-secret `key` query parameter on every price endpoint — confirmed rejects requests without it (`{"error":"unauthorized"}`). Secret stored in Vaultwarden, generated via `openssl rand -hex 16` |
|
|||
|
|
| CORS | `Access-Control-Allow-Origin: *` set explicitly — required because ONLYOFFICE custom functions call `fetch()` from the browser tab, not from a server |
|
|||
|
|
|
|||
|
|
### **Endpoints**
|
|||
|
|
| Endpoint | Params | Behavior |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `/current` | `ticker`, `key` | Today's price. In-memory cache, 15-minute TTL (mutual funds price once/day after close; short cache lets same-day NAV postings show up without waiting a full day). |
|
|||
|
|
| `/historical` | `ticker`, `date` (YYYY-MM-DD), `key` | Closing price on/before the given date — resolves to the nearest prior trading day to survive weekends/holidays. Cached **permanently** in SQLite once resolved, since a past date's close never changes. If `date` is today or in the future, transparently falls back to `/current` logic. |
|
|||
|
|
| `/health` | none | Basic up/down check, no auth required. |
|
|||
|
|
|
|||
|
|
### **Docker Compose Project — stockproxy**
|
|||
|
|
Located at:
|
|||
|
|
```
|
|||
|
|
~/docker/stockproxy/
|
|||
|
|
```
|
|||
|
|
Files: `app.py`, `requirements.txt`, `Dockerfile`, `docker-compose.yml`.
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
services:
|
|||
|
|
stockproxy:
|
|||
|
|
build: .
|
|||
|
|
container_name: stockproxy
|
|||
|
|
restart: unless-stopped
|
|||
|
|
ports:
|
|||
|
|
- "5005:5005"
|
|||
|
|
environment:
|
|||
|
|
- PROXY_SECRET=<secret — stored in Vaultwarden, not in plaintext docs>
|
|||
|
|
- DB_PATH=/data/cache.db
|
|||
|
|
volumes:
|
|||
|
|
- /opt/stockproxy/data:/data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key notes:**
|
|||
|
|
- `Dockerfile` builds on `python:3.12-slim`, installs `flask`/`requests`/`gunicorn`, runs via
|
|||
|
|
`gunicorn --bind 0.0.0.0:5005 --workers 2 --timeout 30 app:app` (production WSGI server,
|
|||
|
|
not Flask's dev server).
|
|||
|
|
- Data volume `/opt/stockproxy/data` on the host (owned by `rufusking`, created via
|
|||
|
|
`sudo mkdir` + `chown` back to the normal user — same permission pattern used elsewhere on
|
|||
|
|
NAS08) holds the permanent SQLite historical-price cache at `cache.db`, so it survives
|
|||
|
|
container rebuilds/restarts.
|
|||
|
|
- No `FINNHUB_API_KEY` variable — Finnhub was dropped from this service entirely after
|
|||
|
|
confirming it has no mutual fund coverage; only Yahoo is used here.
|
|||
|
|
|
|||
|
|
### **NPM / SSL**
|
|||
|
|
Proxied via NPM on HAS — see `server_homeassistant.md` Proxy Hosts table for the entry.
|
|||
|
|
Public DNS (`stocks.kingdezigns.com`, Dreamhost A record) was added to the existing daily
|
|||
|
|
DDNS refresh script alongside the other KingDezigns subdomains.
|
|||
|
|
|
|||
|
|
> **Note:** HTTPS/SSL is required here, not optional — ONLYOFFICE is itself served over
|
|||
|
|
> HTTPS, and browsers block a plain-HTTP `fetch()` call from an HTTPS page as mixed content.
|
|||
|
|
> The proxy host was initially created without SSL, then edited to add **Force SSL** +
|
|||
|
|
> **HTTP/2 Support** once DNS had propagated — same NPM host throughout, not a second one.
|
|||
|
|
|
|||
|
|
### **Verification Commands**
|
|||
|
|
```bash
|
|||
|
|
# Local health check, bypasses NPM/DNS entirely
|
|||
|
|
curl "http://localhost:5005/health"
|
|||
|
|
|
|||
|
|
# Confirm auth is enforced (should return unauthorized, not real data)
|
|||
|
|
curl "https://stocks.kingdezigns.com/current?ticker=MKDVX"
|
|||
|
|
|
|||
|
|
# Real current-price lookup
|
|||
|
|
curl "https://stocks.kingdezigns.com/current?ticker=MKDVX&key=<secret>"
|
|||
|
|
|
|||
|
|
# Real historical lookup
|
|||
|
|
curl "https://stocks.kingdezigns.com/historical?ticker=MKDVX&date=2025-07-31&key=<secret>"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **Known Limitations**
|
|||
|
|
- Yahoo's chart endpoint is unofficial and could change or start blocking the scraping
|
|||
|
|
`User-Agent` pattern at some point — not a documented/supported API. Same risk category as
|
|||
|
|
the PLEX32 `docker pull` intermittent-failure issue: usually reliable, not guaranteed
|
|||
|
|
forever.
|
|||
|
|
- At ~50 tickers, a first-time lookup for a newly-rolled-forward anchor date results in ~50
|
|||
|
|
fresh Yahoo requests in quick succession — not yet stress-tested for Yahoo-side
|
|||
|
|
rate-limiting at that volume. Widespread simultaneous `#ERROR`/`#N/A` across many tickers
|
|||
|
|
(rather than one-off) should be treated as a signal to wait a minute and retry, not a sign
|
|||
|
|
the service is broken.
|
|||
|
|
- Read-only service — no mutation/write endpoints, so no data-integrity risk beyond the
|
|||
|
|
cache itself.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📈 Collabora Online — Stock/Fund Price Macro Integration (2026-07-22)
|
|||
|
|
|
|||
|
|
### **Overview**
|
|||
|
|
As part of the ongoing ONLYOFFICE vs. Collabora reliability trial (see `network_index.md`),
|
|||
|
|
the ONLYOFFICE custom-function stock price integration was ported to Collabora. Originally
|
|||
|
|
this included `FUNDPRICE`/`FUNDPRICE_HIST` (calling STOCKPROXY) plus `STOCKPRICE`/`STOCKTIME`
|
|||
|
|
(calling Finnhub directly) — **Finnhub was removed on 2026-07-23; see the dedicated section
|
|||
|
|
below.** All four spreadsheet functions now call STOCKPROXY exclusively. The STOCKPROXY
|
|||
|
|
backend itself required zero changes for the Collabora port — it's plain HTTPS/JSON and both
|
|||
|
|
editors can call it identically. Only the spreadsheet-side "wrapper" had to be rewritten,
|
|||
|
|
because ONLYOFFICE and Collabora use completely different macro engines (ONLYOFFICE: JS
|
|||
|
|
`Api.AddCustomFunction()` running in the browser; Collabora/LibreOffice: Basic macros running
|
|||
|
|
server-side inside the Collabora container).
|
|||
|
|
|
|||
|
|
**Status: working as of 2026-07-23.** `FUNDPRICE("SMERY")`, `FUNDPRICE_HIST`, and the new
|
|||
|
|
`FUNDTIME` all confirmed working in the Collabora test copy.
|
|||
|
|
|
|||
|
|
### **File Format Requirement — `.ods`, not `.xlsx`**
|
|||
|
|
The Collabora test copy must be saved as **ODF Spreadsheet (`.ods`)**, not `.xlsx`. LibreOffice
|
|||
|
|
Basic modules do not reliably persist across save/close/reopen cycles in `.xlsx` (OOXML) —
|
|||
|
|
confirmed during setup: the module silently vanished from the file every time it was saved as
|
|||
|
|
`.xlsx` and reopened. Switching to native `.ods` fixed this immediately and the module has
|
|||
|
|
persisted reliably since. The ONLYOFFICE production file is unaffected and remains `.xlsx` —
|
|||
|
|
this only applies to the separate Collabora test copy (`stock_tracking_collabora.ods`), which
|
|||
|
|
was always intended to be a separate file per the original trial plan.
|
|||
|
|
|
|||
|
|
### **Macro Module — `StockFunctions.bas`**
|
|||
|
|
LibreOffice Basic port of the ONLYOFFICE custom functions. Stored as a document-embedded Basic
|
|||
|
|
module (Standard library, inside the document itself — not "My Macros"), so it travels with
|
|||
|
|
the `.ods` file.
|
|||
|
|
|
|||
|
|
**Key implementation differences from the ONLYOFFICE JS version:**
|
|||
|
|
- No native `fetch()`/`async` in LibreOffice Basic. HTTP GET is implemented via the
|
|||
|
|
`com.sun.star.ucb.SimpleFileAccess` + `com.sun.star.io.TextInputStream` UNO services,
|
|||
|
|
called synchronously.
|
|||
|
|
- A small hand-rolled `JsonNumber()` helper extracts a numeric value from the flat JSON
|
|||
|
|
response (`{"price": 34.54, ...}`) — no JSON library dependency needed since STOCKPROXY's
|
|||
|
|
response shape is simple and flat. A parallel `JsonString()` helper (added 2026-07-23,
|
|||
|
|
see Finnhub removal section below) extracts quoted string values the same way, for
|
|||
|
|
`resolved_date`.
|
|||
|
|
- A minimal `EncodeUrl()` percent-encoder handles ticker symbols and `yyyy-mm-dd` date strings
|
|||
|
|
in the query string.
|
|||
|
|
- Synchronous execution means a full recalc of a large fund table (e.g. ~50 funds × 2 calls
|
|||
|
|
each) will be noticeably slower than ONLYOFFICE's concurrent browser-side `fetch()` calls —
|
|||
|
|
worth monitoring; STOCKPROXY's 15-minute `/current` cache helps but doesn't eliminate the
|
|||
|
|
round-trip cost per cell.
|
|||
|
|
|
|||
|
|
**Current formula usage** (as of 2026-07-23 — `STOCKPRICE`/`STOCKTIME` removed, see below):
|
|||
|
|
```
|
|||
|
|
=FUNDPRICE(D4)
|
|||
|
|
=FUNDPRICE_HIST(D4,TEXT($B$3,"yyyy-mm-dd"))
|
|||
|
|
=FUNDPRICE(A5) ' equity tickers — replaces old =STOCKPRICE(A5)
|
|||
|
|
=FUNDTIME(A5) ' equity tickers — replaces old =STOCKTIME(A5)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Editing limitation:** Collabora's browser UI (via Nextcloud/`richdocuments`) does not expose
|
|||
|
|
a working Basic IDE — Tools → Macros only offers "Run Macro" (which only lists `Sub`
|
|||
|
|
procedures, not the `Function`s used as spreadsheet formulas — an empty list here is expected,
|
|||
|
|
not an error). Any future edits to `StockFunctions.bas` must be made in **desktop LibreOffice
|
|||
|
|
Calc**, then the `.ods` file re-uploaded to Nextcloud to replace the Collabora copy. Desktop
|
|||
|
|
LibreOffice also requires its own client-side macro security level set to **Medium** (Tools →
|
|||
|
|
Options → LibreOffice → Security → Macro Security) — separate from the server-side setting
|
|||
|
|
below — or macros will be silently blocked with "Macros in this document are disabled due to
|
|||
|
|
the Macro Security settings."
|
|||
|
|
|
|||
|
|
### **Required Collabora Server Config Changes (`coolwsd.xml`)**
|
|||
|
|
Two separate server-side restrictions had to be lifted before the macro would execute. Both
|
|||
|
|
are deliberate security defaults, not bugs — see Security Considerations below.
|
|||
|
|
|
|||
|
|
**1. Macro execution was disabled entirely by default:**
|
|||
|
|
```xml
|
|||
|
|
<enable_macros_execution desc="..." type="bool" default="false">true</enable_macros_execution>
|
|||
|
|
```
|
|||
|
|
`macro_security_level` was left at its default of `1` (Medium) — this means Collabora shows a
|
|||
|
|
one-time "allow macros for this document?" prompt on open, which was intentionally kept rather
|
|||
|
|
than dropping to `0` (no prompt at all).
|
|||
|
|
|
|||
|
|
**2. Network allowlist (`net.lok_allow`) blocked the STOCKPROXY hostname:**
|
|||
|
|
Even with macros enabled, the first real attempt failed with:
|
|||
|
|
```
|
|||
|
|
ERROR 1: An exception occurred Type: com.sun.star.uno.RuntimeException
|
|||
|
|
Message: access to host denied. URL: https://stocks.kingdezigns.com/current?...
|
|||
|
|
```
|
|||
|
|
**Root cause:** `net.lok_allow.host` entries are regex patterns matched against the **literal
|
|||
|
|
URL/hostname string**, not against the DNS-resolved IP. The stock default list only contains
|
|||
|
|
patterns for literal private-IP text (e.g. `192\.168\.[0-9]{1,3}\.[0-9]{1,3}`, `localhost`,
|
|||
|
|
loopback addresses). Because the macro calls `https://stocks.kingdezigns.com/...` (a hostname,
|
|||
|
|
not an IP literal), it never matched any default pattern — even though that hostname resolves
|
|||
|
|
to `192.168.150.30`, a private IP that would have matched if it had been used directly. Calling
|
|||
|
|
the raw IP instead of the hostname was considered and rejected: NPM on HAS routes by hostname,
|
|||
|
|
so an IP-only request would very likely hit the wrong vhost or be rejected by NPM entirely.
|
|||
|
|
|
|||
|
|
**Fix — added a new entry to the existing `<net><lok_allow>` block:**
|
|||
|
|
```xml
|
|||
|
|
<host desc="stockproxy API on NAS08, proxied via NPM on HAS">stocks\.kingdezigns\.com</host>
|
|||
|
|
```
|
|||
|
|
Added directly after the existing `localhost` entry, inside `<lok_allow>`.
|
|||
|
|
|
|||
|
|
**Applying changes to the Collabora container:**
|
|||
|
|
```bash
|
|||
|
|
docker cp collabora:/etc/coolwsd/coolwsd.xml coolwsd.xml
|
|||
|
|
# edit coolwsd.xml
|
|||
|
|
docker cp coolwsd.xml collabora:/etc/coolwsd/coolwsd.xml
|
|||
|
|
docker restart collabora
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **Diagnostic Method — Exposing the Real Error**
|
|||
|
|
The macro's normal error handling (`On Error GoTo ErrHandler`) swallows exceptions into a
|
|||
|
|
generic `#ERROR` cell value, which masked the real cause during initial testing. A temporary
|
|||
|
|
diagnostic Sub was used to surface the actual LibreOffice exception message:
|
|||
|
|
```basic
|
|||
|
|
Sub DebugFundPrice
|
|||
|
|
Dim sUrl As String
|
|||
|
|
sUrl = PROXY_BASE() & "/current?ticker=SMERY&key=" & PROXY_SECRET()
|
|||
|
|
Dim oSFA As Object, oStream As Object
|
|||
|
|
On Error GoTo ErrHandler
|
|||
|
|
oSFA = createUnoService("com.sun.star.ucb.SimpleFileAccess")
|
|||
|
|
oStream = oSFA.openFileRead(sUrl)
|
|||
|
|
MsgBox "SUCCESS - stream opened"
|
|||
|
|
Exit Sub
|
|||
|
|
ErrHandler:
|
|||
|
|
MsgBox "ERROR " & Err & ": " & Error$ & Chr(10) & "URL: " & sUrl
|
|||
|
|
End Sub
|
|||
|
|
```
|
|||
|
|
Run via Tools → Run Macro (visible there since it's a `Sub`, unlike the `Function`s). This is
|
|||
|
|
what revealed the `access to host denied` message and made the `net.lok_allow` root cause
|
|||
|
|
identifiable. **Removed from the module after diagnosis was complete** — it embedded the
|
|||
|
|
proxy secret in plaintext inside a `MsgBox` call, which is fine transiently for local
|
|||
|
|
debugging but shouldn't be left in a saved document.
|
|||
|
|
|
|||
|
|
### **Pi-hole Local DNS Record — Verified Working (was previously unconfirmed)**
|
|||
|
|
The STOCKPROXY section above lists a required Pi-hole local DNS record for
|
|||
|
|
`stocks.kingdezigns.com` → `192.168.150.30`. During this session that record was found to
|
|||
|
|
**not actually be resolving correctly** — `dig @192.168.150.35 stocks.kingdezigns.com` was
|
|||
|
|
returning the public WAN IP (`72.238.137.115`) instead of the internal override, causing the
|
|||
|
|
same NAT-hairpin failure pattern previously seen with `cloud.kingdezigns.com` and
|
|||
|
|
`collabora.kingdezigns.com`.
|
|||
|
|
|
|||
|
|
Pi-hole v6 (Docker) does not have a working `pihole restartdns` subcommand from inside the
|
|||
|
|
container (`docker exec pihole pihole restartdns` fails — command doesn't exist in v6).
|
|||
|
|
`docker exec pihole pihole reloaddns` ran but threw a harmless script error
|
|||
|
|
(`local: FTL_PID_FILE: readonly variable`) — it appeared to still flush/reload correctly in
|
|||
|
|
principle, but a full container restart was used to be certain:
|
|||
|
|
```bash
|
|||
|
|
docker restart pihole
|
|||
|
|
```
|
|||
|
|
After the restart, `dig @192.168.150.35 stocks.kingdezigns.com` correctly returned
|
|||
|
|
`192.168.150.30` with the `aa` (authoritative answer) flag set, confirming Pi-hole is now
|
|||
|
|
serving the local override as intended. **If this record is ever found not resolving locally
|
|||
|
|
again, a full `docker restart pihole` is the confirmed reliable fix** — `reloaddns` may not be
|
|||
|
|
sufcient in Pi-hole v6 containers.
|
|||
|
|
|
|||
|
|
### **Diagnostic Note — "Slow Kit jail setup with copying, cannot bind-mount"**
|
|||
|
|
Collabora's own "Server audit" panel (visible in the browser UI) surfaced this as a red-status
|
|||
|
|
warning during troubleshooting. Investigated and **confirmed unrelated to the network/macro
|
|||
|
|
issue** — this is a well-documented, cosmetic, performance-only Collabora Docker issue
|
|||
|
|
(startup self-test for bind-mount support runs as an unprivileged UID and always fails inside
|
|||
|
|
Docker, falling back to slower file-copying instead of bind-mounts for the per-document jail).
|
|||
|
|
It does not affect network access, macro execution, or document correctness. No action taken;
|
|||
|
|
noted here so it isn't re-investigated as a false lead in the future.
|
|||
|
|
|
|||
|
|
### **Security Considerations**
|
|||
|
|
Both `coolwsd.xml` changes above are server-wide, not scoped to a single document — any file
|
|||
|
|
opened in this Collabora instance can now execute embedded macros with network access to the
|
|||
|
|
allowlisted hosts, not just this spreadsheet. This is a deliberate, accepted tradeoff given
|
|||
|
|
Collabora only serves documents from trusted KingDezigns Nextcloud users, but is worth keeping
|
|||
|
|
in mind:
|
|||
|
|
- `enable_macros_execution` was previously hardened to `false` by default following
|
|||
|
|
**CVE-2025-24796** (a macro-based remote code execution vulnerability affecting Collabora's
|
|||
|
|
jailed macro execution model). `macro_security_level` was deliberately left at `1` (Medium,
|
|||
|
|
prompts before running) rather than `0` (always allow, no prompt) to retain a manual
|
|||
|
|
confirmation step.
|
|||
|
|
- The `net.lok_allow` allowlist exists specifically to limit which hosts a jailed macro can
|
|||
|
|
reach, even with execution enabled — only `stocks.kingdezigns.com` was added, not a broader
|
|||
|
|
wildcard.
|
|||
|
|
|
|||
|
|
### **Open / Follow-up Items**
|
|||
|
|
- Recreate the ~50-fund table and ranking table (`RANK.EQ` + `+ROW()/1000000` tie-breaker) in
|
|||
|
|
the Collabora `.ods` copy — plain spreadsheet formulas, expected to carry over unchanged.
|
|||
|
|
- Stress-test full-sheet recalc performance with the complete fund table, given the
|
|||
|
|
synchronous/blocking nature of Basic macro HTTP calls vs. ONLYOFFICE's concurrent JS.
|
|||
|
|
- Re-test conditional formatting on formula cells in Collabora — this was the original bug
|
|||
|
|
that triggered the ONLYOFFICE vs. Collabora trial and has not yet been re-verified on the
|
|||
|
|
Collabora side with the stock-price formulas in place.
|
|||
|
|
- Confirm recalc-on-file-load behavior (Tools → Options → LibreOffice Calc → Formula →
|
|||
|
|
Recalculation on File Load) is set to always recalculate, to avoid stale values on open.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 RANK.EQ Circular Reference (Err:522) Fix — Static Price Snapshot (2026-07-23)
|
|||
|
|
|
|||
|
|
### **Overview**
|
|||
|
|
The "HSA Investiments Choices" tab uses `RANK.EQ` to rank ~50 funds by percent price
|
|||
|
|
change. The ranking worked correctly on file open but threw **Err:522 (circular
|
|||
|
|
reference)** on the rank column any time *any* cell in the sheet was edited — in both
|
|||
|
|
ONLYOFFICE and the Collabora `.ods` test copy.
|
|||
|
|
|
|||
|
|
### **Root Cause**
|
|||
|
|
The rank formula's range ultimately depended on live, network-backed custom function
|
|||
|
|
calls (`FUNDPRICE`/`FUNDPRICE_HIST`, calling STOCKPROXY — see dedicated sections above).
|
|||
|
|
The chain was:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
P = FUNDPRICE(ticker) ← live network call
|
|||
|
|
Q = FUNDPRICE_HIST(ticker, date) or fallback FUNDPRICE ← live network call
|
|||
|
|
S = (P - historical) / P → % change ← depends on P, Q
|
|||
|
|
RANK.EQ(S4, $S$3:$S$53, 0) ← depends on all 50 S cells
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`RANK.EQ` was not literally self-referential (it lived in a separate column, not inside
|
|||
|
|
`S3:S53`), and swapping it for an equivalent `SUMPRODUCT(($S$3:$S$53>S4))+1` formula
|
|||
|
|
produced the identical Err:522 — which ruled out the ranking function itself as the
|
|||
|
|
cause. The actual problem: on file *load*, the sheet calculates once, top-to-bottom, and
|
|||
|
|
everything settles. On an *edit-triggered* incremental recalc, the engine has to confirm
|
|||
|
|
all 50 live/async price-fetch cells have resolved before it can evaluate a formula that
|
|||
|
|
reads the whole range at once. If any of those cells is still "in flight" when the rank
|
|||
|
|
formula re-evaluates, the dependency graph re-enters a cell that's mid-calculation and the
|
|||
|
|
engine reports it as circular reference rather than "value pending."
|
|||
|
|
|
|||
|
|
**Confirmed via isolated test:** pointing a rank formula at a column of plain static
|
|||
|
|
numbers (no formulas at all) instead of the live `S` column did **not** error on edit —
|
|||
|
|
this isolated the cause to the live/async dependency chain, not the rank formula syntax.
|
|||
|
|
|
|||
|
|
### **Fix — Manual Snapshot Macro**
|
|||
|
|
Rather than ranking directly against the live `FUNDPRICE`/`FUNDPRICE_HIST` chain, a
|
|||
|
|
button-triggered macro copies the **calculated values** (not formulas) of the live price
|
|||
|
|
columns into new static columns, and all downstream percent-change/rank formulas were
|
|||
|
|
rebuilt to read from the static columns instead.
|
|||
|
|
|
|||
|
|
**Column layout (spreadsheet-side):**
|
|||
|
|
| Column | Contents |
|
|||
|
|
|---|---|
|
|||
|
|
| `P` | Live: `=FUNDPRICE(N3)` — current price (unchanged) |
|
|||
|
|
| `Q` | Live: `=IF(N3="","",IF(FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))="#N/A",FUNDPRICE(N3),FUNDPRICE_HIST(N3,TEXT($B$5,"yyyy-mm-dd"))))` — historical price (unchanged) |
|
|||
|
|
| `R` | Static snapshot of `P`, values only, written by macro |
|
|||
|
|
| `S` | Static snapshot of `Q`, values only, written by macro |
|
|||
|
|
| `T` | `=R3-S3` — dollar difference, now built from static inputs |
|
|||
|
|
| `U` | `=T3/R3` — percent change, calculated from static inputs |
|
|||
|
|
| Rank column | `=RANK.EQ(U4,$U$3:$U$53,0)` — now depends only on the static chain |
|
|||
|
|
|
|||
|
|
`P` and `Q` are untouched and continue to refresh live/normally whenever the sheet
|
|||
|
|
recalculates. Only `R`/`S` are static, and they are only ever refreshed when the macro is
|
|||
|
|
explicitly run — decoupling the rank calculation from the live network-fetch chain
|
|||
|
|
entirely, so ordinary cell edits no longer trigger Err:522.
|
|||
|
|
|
|||
|
|
**Working macro (LibreOffice Basic, run via a sheet button):**
|
|||
|
|
```basic
|
|||
|
|
REM ***** BASIC *****
|
|||
|
|
Sub CopyStockValues
|
|||
|
|
Dim oDoc As Object
|
|||
|
|
Dim oSheet As Object
|
|||
|
|
Dim oSource As Object
|
|||
|
|
Dim oDest As Object
|
|||
|
|
oDoc = ThisComponent
|
|||
|
|
oSheet = oDoc.CurrentController.ActiveSheet
|
|||
|
|
' Source: P3:Q53
|
|||
|
|
oSource = oSheet.getCellRangeByName("P3:Q53")
|
|||
|
|
' Destination: R3:S53
|
|||
|
|
oDest = oSheet.getCellRangeByName("R3:S53")
|
|||
|
|
' Copy only the calculated values (no formulas or formatting)
|
|||
|
|
oDest.setDataArray(oSource.getDataArray())
|
|||
|
|
End Sub
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Bound to a sheet button so the user can trigger a refresh on demand. Since mutual fund
|
|||
|
|
NAVs only post once per day after market close (per the `FUNDPRICE` behavior documented
|
|||
|
|
above), a once- or twice-daily manual click is sufficient — the sheet can otherwise be
|
|||
|
|
freely edited all day without the rank column breaking.
|
|||
|
|
|
|||
|
|
### **Diagnostic Dead Ends / Notes for Future Troubleshooting**
|
|||
|
|
- **`ThisComponent.Sheets.getByIndex(0)` is not reliable** if the workbook has multiple
|
|||
|
|
tabs — an early draft of the macro wrote to sheet index 0 while the user was viewing a
|
|||
|
|
different tab, silently "succeeding" (no error, message box fired) while writing to the
|
|||
|
|
wrong (effectively invisible) location. Use
|
|||
|
|
`oDoc.CurrentController.ActiveSheet` instead to always target the tab currently in view.
|
|||
|
|
- **Recalculation-before-macro-execution can look like a hang.** A `For...Next` loop using
|
|||
|
|
`getCellByPosition().getValue()` on live `FUNDPRICE`/`FUNDPRICE_HIST` cells took ~5
|
|||
|
|
minutes to process just 2 rows — consistent with LibreOffice forcing a full sheet
|
|||
|
|
recalculation (all ~50 rows × up to 2 live network calls each, executed synchronously)
|
|||
|
|
before the macro body even began. Check Tools → Options → LibreOffice Calc → Formula →
|
|||
|
|
recalculation-on-load/macro settings if a snapshot macro appears to hang.
|
|||
|
|
- **`setDataArray()` bulk copy succeeded where a cell-by-cell `getValue()`/`setValue()`
|
|||
|
|
loop did not** (the loop version produced no visible error but also wrote nothing
|
|||
|
|
usable). The working, confirmed macro uses
|
|||
|
|
`oDest.setDataArray(oSource.getDataArray())` on the full `P3:Q53` → `R3:S53` range in a
|
|||
|
|
single call rather than iterating cell-by-cell — this is now the standard pattern for
|
|||
|
|
any future static-snapshot needs in this workbook.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📉 Collabora — Finnhub Removed, STOCKPRICE/STOCKTIME Replaced with FUNDPRICE/FUNDTIME (2026-07-23)
|
|||
|
|
|
|||
|
|
### **Overview**
|
|||
|
|
`STOCKPRICE(A5)` and `STOCKTIME(A5)` (the two Collabora functions calling Finnhub directly
|
|||
|
|
for equity tickers) began returning `#ERROR`. Root cause matched the earlier `FUNDPRICE`
|
|||
|
|
allowlist issue: Finnhub's hostname (`finnhub.io`) had never been added to Collabora's
|
|||
|
|
`net.lok_allow` allowlist in `coolwsd.xml` when the macros were first ported, so every call
|
|||
|
|
was blocked with `access to host denied`, surfaced generically as `#ERROR` by the module's
|
|||
|
|
error handling (same swallowing behavior documented in the ONLYOFFICE troubleshooting
|
|||
|
|
history above).
|
|||
|
|
|
|||
|
|
Rather than add a second allowlist entry for Finnhub, **Finnhub was removed from this
|
|||
|
|
workbook entirely.** Finnhub was only ever used for `STOCKPRICE`/`STOCKTIME` in the first
|
|||
|
|
place because, at the time, it was believed to offer better equity coverage — but it was
|
|||
|
|
already confirmed to have **no historical price data**, which is the original reason
|
|||
|
|
STOCKPROXY was built. Since STOCKPROXY's `/current` endpoint returned valid data for a plain
|
|||
|
|
equity ticker (`SMERY`) with no fund-vs-stock distinction in the API itself, there was no
|
|||
|
|
remaining reason to keep a second price backend or a second allowlist entry.
|
|||
|
|
|
|||
|
|
### **Change Made**
|
|||
|
|
- `STOCKPRICE(ticker)` — **removed**. `FUNDPRICE(ticker)` now serves both fund and equity
|
|||
|
|
tickers; STOCKPROXY does not distinguish between them.
|
|||
|
|
- `STOCKTIME(ticker)` — **removed**, replaced by a new `FUNDTIME(ticker)` function that
|
|||
|
|
calls the same `/current` endpoint as `FUNDPRICE` and extracts the `resolved_date` field
|
|||
|
|
instead of `price`.
|
|||
|
|
- `FINNHUB_KEY()` config function — **removed**, no longer referenced anywhere in the module.
|
|||
|
|
- New helper `JsonString(sJson, sKey)` added alongside the existing `JsonNumber()` — extracts
|
|||
|
|
a quoted string value from the flat JSON response (`resolved_date` is a string, not a bare
|
|||
|
|
number, so the existing numeric parser couldn't read it).
|
|||
|
|
|
|||
|
|
**Spreadsheet formula changes required (Dashboard tab):**
|
|||
|
|
| Old | New |
|
|||
|
|
|---|---|
|
|||
|
|
| `=STOCKPRICE(A5)` | `=FUNDPRICE(A5)` |
|
|||
|
|
| `=STOCKTIME(A5)` | `=FUNDTIME(A5)` |
|
|||
|
|
|
|||
|
|
### **Known Limitation — `FUNDTIME` Returns a Date, Not a Timestamp**
|
|||
|
|
STOCKPROXY's `/current` response includes `resolved_date` (`YYYY-MM-DD`) but no time-of-day
|
|||
|
|
field. Finnhub's old `STOCKTIME` returned a full Unix timestamp (`t` field) with intraday
|
|||
|
|
resolution, which mattered less for mutual funds (priced once/day) but could matter for
|
|||
|
|
actively-traded equities. This is an accepted tradeoff for now. If time-of-day resolution is
|
|||
|
|
ever needed, STOCKPROXY's Flask app would need a real timestamp field added to the `/current`
|
|||
|
|
response — a small backend change, not yet scoped or requested.
|
|||
|
|
|
|||
|
|
### **Result**
|
|||
|
|
Confirmed working — no `#ERROR` after the swap, both on `SMERY` and equity tickers via
|
|||
|
|
`FUNDPRICE`/`FUNDTIME`. `finnhub\.io` was never actually added to `net.lok_allow` (the
|
|||
|
|
allowlist edit was skipped in favor of removing the Finnhub dependency outright), so no
|
|||
|
|
`coolwsd.xml` changes were required for this fix — only the Basic module and the two
|
|||
|
|
spreadsheet cell formulas changed.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🤖 Synaplan (AI Document Summarization) — Connector Installed, Backend Not Deployed
|
|||
|
|
|
|||
|
|
The `synaplan_integration` Nextcloud app is installed and enabled, providing a "Summarize with Synaplan" action on files. **This is currently non-functional** — the app is only a thin client/connector; it requires a separate, full Synaplan server stack to be deployed and reachable.
|
|||
|
|
|
|||
|
|
### **What's Missing**
|
|||
|
|
Synaplan's full self-hosted stack (per synaplan.com) consists of:
|
|||
|
|
- PHP backend
|
|||
|
|
- Vue.js frontend
|
|||
|
|
- MariaDB (separate database instance from Nextcloud's)
|
|||
|
|
- Qdrant (vector database, for RAG/semantic search)
|
|||
|
|
- Optionally Ollama (for local AI inference, avoiding external API calls)
|
|||
|
|
|
|||
|
|
None of these components exist anywhere on the KingDezigns network as of 2026-07-18. The Nextcloud app currently points at a default `http://localhost:8000/api/v1/files/upload`, which fails with connection refused since nothing is listening there.
|
|||
|
|
|
|||
|
|
### **Status: Deferred**
|
|||
|
|
Deploying the full Synaplan stack is a separate project (additional containers, database, storage planning, and a decision on local vs. cloud AI model routing) — not a quick config fix. Revisit when ready to scope it properly.
|
|||
|
|
|
|||
|
|
### **Error Reference (for future troubleshooting)**
|
|||
|
|
| Error seen | Cause |
|
|||
|
|
|---|---|
|
|||
|
|
| `Host "localhost" violates local access rules` | Nextcloud's SSRF protection (`allow_local_remote_servers`) blocked the request before `allow_local_remote_servers` was set to `true` |
|
|||
|
|
| `cURL error 7: Failed to connect to localhost port 8000` | Confirmed no Synaplan backend server exists/listens on the expected port — the connector app was installed without its required backend service |
|
|||
|
|
|
|||
|
|
`allow_local_remote_servers` was set to `true` via:
|
|||
|
|
```bash
|
|||
|
|
sudo docker exec -u www-data nextcloud php occ config:system:set allow_local_remote_servers --type=bool --value=true
|
|||
|
|
```
|
|||
|
|
This setting should remain enabled if/when the Synaplan backend is deployed locally.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔒 Required Firewall Behavior
|
|||
|
|
|
|||
|
|
### **Inbound to NAS08**
|
|||
|
|
- Allowed from VLAN 1 and VLAN 20 (due to global rules)
|
|||
|
|
- Allowed from VLAN 50 (local VLAN)
|
|||
|
|
- IoT VLAN 30 → DNS allowed (global DNS rule)
|
|||
|
|
- Guest VLAN 40 → DNS allowed (global DNS rule)
|
|||
|
|
- PLEX32 (192.168.150.45) → SSH allowed (local VLAN 50) — required for backup disk stats
|
|||
|
|
|
|||
|
|
### **Outbound from NAS08**
|
|||
|
|
- Subject to VLAN 50's global final drop:
|
|||
|
|
- Cannot initiate to VLANs 1, 10, 20, 30, 40
|
|||
|
|
- Allowed exceptions:
|
|||
|
|
- DNS responses
|
|||
|
|
- Local VLAN 50 traffic
|
|||
|
|
|
|||
|
|
### **Critical Requirement**
|
|||
|
|
- Must remain reachable for DNS from **all VLANs**.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔐 Security Measures
|
|||
|
|
- **Fail2Ban enabled**
|
|||
|
|
- 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
|