diff --git a/.config/home-manager/home.nix b/.config/home-manager/home.nix index ae34554..0b50e10 100644 --- a/.config/home-manager/home.nix +++ b/.config/home-manager/home.nix @@ -1,4 +1,4 @@ -{ config, pkgs, ... }: +{ config, pkgs, lib, ... }: let sys = (import { }).config; @@ -15,6 +15,7 @@ in { git-crypt # encrypted files in public git repos jq # json query tool keychain # ssh-agent + python3 # various scripts rclone # sync with nextcloud screen # terminal window manager unison # sync with other machines @@ -33,6 +34,12 @@ in { home.sessionVariables = { EDITOR = config.programs.zsh.shellAliases.vi; + + NOCODB_TOKEN = + let tokenFile = "${config.home.homeDirectory}/.nocodb"; in + if builtins.pathExists tokenFile + then lib.removeSuffix "\n" (builtins.readFile tokenFile) + else ""; }; programs.gpg.enable = true; diff --git a/.gitattributes b/.gitattributes index ce57b29..408306a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ +/.nocodb filter=git-crypt diff=git-crypt /.pvs-license filter=git-crypt diff=git-crypt /.reminders filter=git-crypt diff=git-crypt diff --git a/.nocodb b/.nocodb new file mode 100644 index 0000000..4ce54b0 Binary files /dev/null and b/.nocodb differ diff --git a/code/kubes/nextcloud/nextcloud/nextcloud-ingress.yaml b/code/kubes/nextcloud/nextcloud/nextcloud-ingress.yaml index a5b2172..468bf74 100644 --- a/code/kubes/nextcloud/nextcloud/nextcloud-ingress.yaml +++ b/code/kubes/nextcloud/nextcloud/nextcloud-ingress.yaml @@ -6,7 +6,7 @@ metadata: name: nextcloud-ingress annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/proxy-body-size: 20m + nginx.ingress.kubernetes.io/proxy-body-size: "4096m" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" spec: ingressClassName: nginx diff --git a/code/kubes/nextcloud/nextcloud/nginx-config.yaml b/code/kubes/nextcloud/nextcloud/nginx-config.yaml index 51441a5..cc769c6 100644 --- a/code/kubes/nextcloud/nextcloud/nginx-config.yaml +++ b/code/kubes/nextcloud/nextcloud/nginx-config.yaml @@ -25,6 +25,9 @@ data: application/javascript mjs; } + # set max upload size + client_max_body_size 4096M; + sendfile on; #tcp_nopush on; @@ -39,8 +42,6 @@ data: server { listen 80; - # set max upload size - client_max_body_size 512M; fastcgi_buffers 64 4K; # Enable gzip but do not remove ETag headers diff --git a/code/kubes/nocodb/namespace.yaml b/code/kubes/nocodb/namespace.yaml new file mode 100644 index 0000000..5b27bab --- /dev/null +++ b/code/kubes/nocodb/namespace.yaml @@ -0,0 +1,7 @@ +--- +kind: Namespace +apiVersion: v1 +metadata: + name: nocodb + annotations: + linkerd.io/inject: enabled diff --git a/code/kubes/nocodb/nocodb/nocodb-ingress.yaml b/code/kubes/nocodb/nocodb/nocodb-ingress.yaml new file mode 100644 index 0000000..0e15b1c --- /dev/null +++ b/code/kubes/nocodb/nocodb/nocodb-ingress.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + namespace: nocodb + name: nocodb-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/proxy-body-size: "4096m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "300" +spec: + ingressClassName: nginx + tls: + - hosts: + - nocodb.xinutec.org + secretName: nocodb-tls + rules: + - host: nocodb.xinutec.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nocodb-server + port: + number: 8080 diff --git a/code/kubes/nocodb/nocodb/nocodb-server.yaml b/code/kubes/nocodb/nocodb/nocodb-server.yaml new file mode 100644 index 0000000..2f1ac86 --- /dev/null +++ b/code/kubes/nocodb/nocodb/nocodb-server.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: nocodb + name: nocodb-server + labels: + app: nocodb +spec: + selector: + matchLabels: + pod-label: nocodb-server-pod + template: + metadata: + labels: + pod-label: nocodb-server-pod + spec: + volumes: + # Create the shared files volume to be used in both pods + - name: server-storage + persistentVolumeClaim: + claimName: nocodb-storage + containers: + - name: nocodb + image: nocodb/nocodb:latest + imagePullPolicy: Always + volumeMounts: + - name: server-storage + mountPath: /usr/app/data + subPath: server-data +--- +apiVersion: v1 +kind: Service +metadata: + namespace: nocodb + name: nocodb-server + labels: + app: nocodb +spec: + selector: + pod-label: nocodb-server-pod + ports: + - protocol: TCP + port: 8080 diff --git a/code/kubes/nocodb/nocodb/nocodb-shared-pvc.yaml b/code/kubes/nocodb/nocodb/nocodb-shared-pvc.yaml new file mode 100644 index 0000000..9bf2231 --- /dev/null +++ b/code/kubes/nocodb/nocodb/nocodb-shared-pvc.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + namespace: nocodb + name: nocodb-storage + labels: + app: nocodb +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 200Gi diff --git a/code/kubes/nocodb/today.py b/code/kubes/nocodb/today.py new file mode 100755 index 0000000..2390163 --- /dev/null +++ b/code/kubes/nocodb/today.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import http.client +import json +import os +import urllib.parse + +RDI_TABLE = "mamm9dpys1j1znw" + +DISHES_TABLE = "m5ddsw6b8q0jn55" + +MEALS_TABLE = "mohjrlr1l7et77g" +MEALS_VIEW_TODAY = "vwpckbejvg1mbifx" +MEALS_LINK_DISHES = "cc9wqhoe8nfb18o" + +def process_nutrients(rdi, ingredient): + nutrients = {} + ingredient_amount = ingredient["Amount (g)"] + for k, v in ingredient.items(): + if v is None or k in ("Amount (g)", "Name"): + nutrients[k] = v + elif k in rdi: + amount = v / 100 * ingredient_amount + nutrients[k] = {"Amount": amount, "RDI": amount / rdi[k]["Amount"]} + return nutrients + + +def sum_amounts(total, dishes): + for dish in dishes: + for k, v in dish.items(): + if isinstance(v, dict): + if k not in total: + total[k] = {"Amount": 0, "RDI": 0} + total[k]["Amount"] += v["Amount"] + total[k]["RDI"] += v["RDI"] + + +def compute_total(dishes): + total = {"Name": "Total", "Amount (g)": None} + sum_amounts(total, dishes) + dishes.append(total) + return dishes + + +class HealthDb: + def __init__(self): + self.conn = http.client.HTTPSConnection("nocodb.xinutec.org") + self.headers = {'xc-token': os.environ["NOCODB_TOKEN"]} + + def get(self, url: str): + self.conn.request("GET", url, headers=self.headers) + + res = self.conn.getresponse() + return json.loads(res.read().decode("utf-8"))["list"] + + def list_table_records(self, table: str, view: str = "", fields: tuple[str] = tuple(), key: str = "Id"): + return { + rec[key]: rec + for rec in self.get(f"/api/v2/tables/{table}/records?offset=0&limit=1000&viewId={view}&fields={','.join(map(urllib.parse.quote_plus, fields))}") + } + + def list_linked_records(self, table: str, link_field_id: str, record_id: int, key: str = "Id"): + return {rec[key]: rec for rec in self.get(f"/api/v2/tables/{table}/links/{link_field_id}/records/{record_id}")} + + def today(self): + rdi = self.list_table_records(RDI_TABLE, fields=("Nutrient", "Amount"), key="Nutrient") + dishes = self.list_table_records(DISHES_TABLE, fields=("Id", "Amount (g)", "nc_cupq___nc_m2m_8u3b_5gyns")) + meals = self.list_table_records(MEALS_TABLE, view=MEALS_VIEW_TODAY) + for mealId in [meal["Id"] for meal in meals.values()]: + meals[mealId]["Dishes"] = [process_nutrients(rdi, { + "Amount (g)": dishes[k]["Amount (g)"], + **dishes[k]["nc_cupq___nc_m2m_8u3b_5gyns"][0]["Ingredients"] + }) for k in self.list_linked_records(MEALS_TABLE, MEALS_LINK_DISHES, mealId).keys()] + table = {} + for meal in meals.values(): + table[meal["Meal"]] = compute_total(meal["Dishes"]) + return table + + +def format_amount(amount): + if isinstance(amount, dict): + rdi = int(round(amount['RDI'] * 100, 0)) + return f"{round(amount['Amount'], 2):7} ({rdi:3}%)" + elif isinstance(amount, int): + return f"{round(amount, 2):7}" + else: + return "" + + +def print_dishes(meal, dishes): + print(f"\n## {meal}\n") + keys = [k for k in dishes[0].keys() if k not in ("Name",)] + padding = max(len(k) for k in keys) + title = "| " + " " * padding + " | " + " | ".join(f"{dish['Name']:18}" for dish in dishes) + print(title) + print("| :" + "-" * (padding - 1) + " | " + " | ".join("-" * 17 + ":" for dish in dishes)) + for k in keys: + print(f"| {k:{padding}} | " + " | ".join(f"{format_amount(dish.get(k, None)):18}" for dish in dishes)) + +db = HealthDb() + +total = {"Name": "Total"} + +for meal, dishes in db.today().items(): + print_dishes(meal, dishes) + + sum_amounts(total, (dish for dish in dishes if dish["Name"] == "Total")) + +print_dishes("Total", [total])