Compare commits
5 commits
2a7124d266
...
4f71ce6bc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f71ce6bc5 | ||
|
|
0044c3a8ea | ||
|
|
4dff834c8a | ||
|
|
5ac8d625b9 | ||
|
|
58cced5dab |
4 changed files with 499 additions and 31 deletions
|
|
@ -546,7 +546,7 @@ module "ingress_api" {
|
|||
ingress_path = ["/api/"]
|
||||
tls_secret_name = var.tls_secret_name
|
||||
# auth = "none": XHR-based API endpoints; forward-auth 302+cookie-dance breaks CORS preflight and browser fetch().
|
||||
auth = "none"
|
||||
auth = "none"
|
||||
}
|
||||
|
||||
# Plan-time read of the ESO-created K8s Secret for Grafana datasource
|
||||
|
|
@ -605,3 +605,318 @@ resource "kubernetes_config_map" "grafana_fire_planner_datasource" {
|
|||
|
||||
# CI retrigger 2026-05-16T13:42:57+00:00 — bulk enrollment apply (pipeline #689 killed)
|
||||
# CI retrigger v2 2026-05-16T13:46:35+00:00
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Reddit FIRE examples ingest — Job (bulk, toggled) + weekly CronJob
|
||||
# Backs the fire_planner.examples module. See:
|
||||
# ~/code/fire-planner/docs/plans/2026-05-28-reddit-examples-{design,plan}.md
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
variable "llama_cpp_base_url" {
|
||||
type = string
|
||||
description = "llama-cpp /v1/chat/completions endpoint for primary LLM extraction"
|
||||
default = "http://llama-cpp.llama-cpp.svc.cluster.local:8000/v1/chat/completions"
|
||||
}
|
||||
|
||||
variable "claude_agent_service_url" {
|
||||
type = string
|
||||
description = "claude-agent-service /v1/chat/completions endpoint for Tier 2 fallback"
|
||||
default = "http://claude-agent-service.claude-agent.svc.cluster.local:8080/v1/chat/completions"
|
||||
}
|
||||
|
||||
variable "run_examples_bulk_ingest" {
|
||||
type = bool
|
||||
description = "Flip to true once to bulk-populate fire_example. Reset to false after."
|
||||
default = false
|
||||
}
|
||||
|
||||
# Reddit OAuth creds pulled from Vault secret/viktor.
|
||||
resource "kubernetes_manifest" "external_secret_examples_reddit" {
|
||||
manifest = {
|
||||
apiVersion = "external-secrets.io/v1beta1"
|
||||
kind = "ExternalSecret"
|
||||
metadata = {
|
||||
name = "fire-planner-examples-reddit"
|
||||
namespace = local.namespace
|
||||
}
|
||||
spec = {
|
||||
refreshInterval = "1h"
|
||||
secretStoreRef = {
|
||||
name = "vault-kv"
|
||||
kind = "ClusterSecretStore"
|
||||
}
|
||||
target = {
|
||||
name = "fire-planner-examples-reddit"
|
||||
}
|
||||
data = [
|
||||
{
|
||||
secretKey = "REDDIT_CLIENT_ID"
|
||||
remoteRef = {
|
||||
key = "viktor"
|
||||
property = "trading_bot_reddit_client_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
secretKey = "REDDIT_CLIENT_SECRET"
|
||||
remoteRef = {
|
||||
key = "viktor"
|
||||
property = "trading_bot_reddit_client_secret"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
depends_on = [kubernetes_namespace.fire_planner]
|
||||
}
|
||||
|
||||
# claude-agent-service bearer pulled separately so its rotation cadence
|
||||
# is decoupled from the Reddit creds.
|
||||
resource "kubernetes_manifest" "external_secret_examples_claude" {
|
||||
manifest = {
|
||||
apiVersion = "external-secrets.io/v1beta1"
|
||||
kind = "ExternalSecret"
|
||||
metadata = {
|
||||
name = "fire-planner-examples-claude"
|
||||
namespace = local.namespace
|
||||
}
|
||||
spec = {
|
||||
refreshInterval = "1h"
|
||||
secretStoreRef = {
|
||||
name = "vault-kv"
|
||||
kind = "ClusterSecretStore"
|
||||
}
|
||||
target = {
|
||||
name = "fire-planner-examples-claude"
|
||||
}
|
||||
data = [
|
||||
{
|
||||
secretKey = "CLAUDE_AGENT_BEARER"
|
||||
remoteRef = {
|
||||
key = "claude-agent-service"
|
||||
property = "api_bearer_token"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
depends_on = [kubernetes_namespace.fire_planner]
|
||||
}
|
||||
|
||||
# Bulk one-shot Job — toggled via var.run_examples_bulk_ingest. Flip to
|
||||
# true once, apply, wait for completion, flip back. The timestamp() in
|
||||
# the name ensures Terraform creates a fresh Job on each (true)
|
||||
# transition rather than refusing to recreate an existing one.
|
||||
resource "kubernetes_job_v1" "examples_bulk_ingest" {
|
||||
count = var.run_examples_bulk_ingest ? 1 : 0
|
||||
metadata {
|
||||
name = "fire-planner-examples-bulk-${formatdate("YYYYMMDDhhmm", timestamp())}"
|
||||
namespace = kubernetes_namespace.fire_planner.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
backoff_limit = 0
|
||||
template {
|
||||
metadata {
|
||||
labels = local.labels
|
||||
}
|
||||
spec {
|
||||
restart_policy = "OnFailure"
|
||||
image_pull_secrets {
|
||||
name = "registry-credentials"
|
||||
}
|
||||
container {
|
||||
name = "ingest"
|
||||
image = local.image
|
||||
image_pull_policy = "IfNotPresent"
|
||||
command = ["python", "-m", "fire_planner", "examples", "ingest",
|
||||
"--top=all,year", "--limit=1000"]
|
||||
|
||||
# DB plumbing — mirror the fire_planner_recompute CronJob.
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "fire-planner-secrets"
|
||||
}
|
||||
}
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "fire-planner-db-creds"
|
||||
}
|
||||
}
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "wealthfolio-sync-db-creds"
|
||||
}
|
||||
}
|
||||
|
||||
# Examples-specific vars.
|
||||
env {
|
||||
name = "REDDIT_CLIENT_ID"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-reddit"
|
||||
key = "REDDIT_CLIENT_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "REDDIT_CLIENT_SECRET"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-reddit"
|
||||
key = "REDDIT_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "CLAUDE_AGENT_BEARER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-claude"
|
||||
key = "CLAUDE_AGENT_BEARER"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "REDDIT_USER_AGENT"
|
||||
value = "fire-planner/0.1"
|
||||
}
|
||||
env {
|
||||
name = "LLAMA_CPP_BASE_URL"
|
||||
value = var.llama_cpp_base_url
|
||||
}
|
||||
env {
|
||||
name = "CLAUDE_AGENT_SERVICE_URL"
|
||||
value = var.claude_agent_service_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# The name embeds a timestamp so a re-plan after time has passed
|
||||
# would otherwise propose a no-op rename. Ignore.
|
||||
# KYVERNO_LIFECYCLE_V1
|
||||
ignore_changes = [
|
||||
metadata[0].name,
|
||||
spec[0].template[0].spec[0].dns_config,
|
||||
]
|
||||
}
|
||||
depends_on = [
|
||||
kubernetes_manifest.external_secret,
|
||||
kubernetes_manifest.db_external_secret,
|
||||
kubernetes_manifest.wealthfolio_sync_db_external_secret,
|
||||
kubernetes_manifest.external_secret_examples_reddit,
|
||||
kubernetes_manifest.external_secret_examples_claude,
|
||||
]
|
||||
}
|
||||
|
||||
# Weekly delta — top-of-week milestone posts. Sunday 04:00 UTC.
|
||||
resource "kubernetes_cron_job_v1" "examples_weekly_delta" {
|
||||
metadata {
|
||||
name = "fire-planner-examples-weekly"
|
||||
namespace = kubernetes_namespace.fire_planner.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
schedule = "0 4 * * 0"
|
||||
concurrency_policy = "Forbid"
|
||||
successful_jobs_history_limit = 3
|
||||
failed_jobs_history_limit = 3
|
||||
job_template {
|
||||
metadata {
|
||||
labels = local.labels
|
||||
}
|
||||
spec {
|
||||
backoff_limit = 0
|
||||
ttl_seconds_after_finished = 86400
|
||||
template {
|
||||
metadata {
|
||||
labels = local.labels
|
||||
}
|
||||
spec {
|
||||
restart_policy = "OnFailure"
|
||||
image_pull_secrets {
|
||||
name = "registry-credentials"
|
||||
}
|
||||
container {
|
||||
name = "ingest"
|
||||
image = local.image
|
||||
image_pull_policy = "IfNotPresent"
|
||||
command = ["python", "-m", "fire_planner", "examples", "ingest",
|
||||
"--top=week", "--limit=200"]
|
||||
|
||||
# DB plumbing — mirror the fire_planner_recompute CronJob.
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "fire-planner-secrets"
|
||||
}
|
||||
}
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "fire-planner-db-creds"
|
||||
}
|
||||
}
|
||||
env_from {
|
||||
secret_ref {
|
||||
name = "wealthfolio-sync-db-creds"
|
||||
}
|
||||
}
|
||||
|
||||
# Examples-specific vars — keep in sync with the bulk Job.
|
||||
env {
|
||||
name = "REDDIT_CLIENT_ID"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-reddit"
|
||||
key = "REDDIT_CLIENT_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "REDDIT_CLIENT_SECRET"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-reddit"
|
||||
key = "REDDIT_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "CLAUDE_AGENT_BEARER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "fire-planner-examples-claude"
|
||||
key = "CLAUDE_AGENT_BEARER"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "REDDIT_USER_AGENT"
|
||||
value = "fire-planner/0.1"
|
||||
}
|
||||
env {
|
||||
name = "LLAMA_CPP_BASE_URL"
|
||||
value = var.llama_cpp_base_url
|
||||
}
|
||||
env {
|
||||
name = "CLAUDE_AGENT_SERVICE_URL"
|
||||
value = var.claude_agent_service_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
# KYVERNO_LIFECYCLE_V1
|
||||
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
kubernetes_manifest.external_secret,
|
||||
kubernetes_manifest.db_external_secret,
|
||||
kubernetes_manifest.wealthfolio_sync_db_external_secret,
|
||||
kubernetes_manifest.external_secret_examples_reddit,
|
||||
kubernetes_manifest.external_secret_examples_claude,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1760,7 +1760,7 @@
|
|||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawSql": "WITH active_count AS (SELECT COUNT(*) AS n FROM accounts), max_complete AS (SELECT MAX(valuation_date) AS d FROM (SELECT d.valuation_date, COUNT(*) AS c FROM dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), yearly AS (SELECT EXTRACT(YEAR FROM valuation_date)::int AS yr, valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date), endpoints AS (SELECT yr, (array_agg(nw ORDER BY valuation_date ASC))[1] AS nw_start, (array_agg(nw ORDER BY valuation_date DESC))[1] AS nw_end, (array_agg(contrib ORDER BY valuation_date ASC))[1] AS contrib_start, (array_agg(contrib ORDER BY valuation_date DESC))[1] AS contrib_end FROM yearly GROUP BY yr) SELECT yr::text AS year, ROUND((contrib_end - contrib_start)::numeric, 0) AS contributions, ROUND((nw_end - nw_start - (contrib_end - contrib_start))::numeric, 0) AS market_gain FROM endpoints ORDER BY yr"
|
||||
"rawSql": "WITH active_count AS (SELECT COUNT(*) AS n FROM accounts), max_complete AS (SELECT MAX(valuation_date) AS d FROM (SELECT d.valuation_date, COUNT(*) AS c FROM dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date), year_end AS (SELECT DISTINCT ON (EXTRACT(YEAR FROM valuation_date)) EXTRACT(YEAR FROM valuation_date)::int AS yr, nw, contrib FROM daily ORDER BY EXTRACT(YEAR FROM valuation_date), valuation_date DESC), deltas AS (SELECT yr, nw, contrib, lag(nw) OVER (ORDER BY yr) AS prev_nw, lag(contrib) OVER (ORDER BY yr) AS prev_contrib FROM year_end) SELECT yr::text AS year, ROUND((contrib - prev_contrib)::numeric, 0) AS contributions, ROUND(((nw - prev_nw) - (contrib - prev_contrib))::numeric, 0) AS market_gain FROM deltas WHERE prev_nw IS NOT NULL ORDER BY yr"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1865,7 +1865,7 @@
|
|||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"rawSql": "WITH active_count AS (SELECT COUNT(*) AS n FROM accounts), max_complete AS (SELECT MAX(valuation_date) AS d FROM (SELECT d.valuation_date, COUNT(*) AS c FROM dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), monthly AS (SELECT date_trunc('month', valuation_date)::date AS month, valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date), endpoints AS (SELECT month, (array_agg(nw ORDER BY valuation_date ASC))[1] AS nw_start, (array_agg(nw ORDER BY valuation_date DESC))[1] AS nw_end, (array_agg(contrib ORDER BY valuation_date ASC))[1] AS contrib_start, (array_agg(contrib ORDER BY valuation_date DESC))[1] AS contrib_end FROM monthly GROUP BY month) SELECT month::timestamp AS time, ROUND((contrib_end - contrib_start)::numeric, 0) AS contributions, ROUND((nw_end - nw_start - (contrib_end - contrib_start))::numeric, 0) AS market_gain FROM endpoints ORDER BY month"
|
||||
"rawSql": "WITH active_count AS (SELECT COUNT(*) AS n FROM accounts), max_complete AS (SELECT MAX(valuation_date) AS d FROM (SELECT d.valuation_date, COUNT(*) AS c FROM dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date), month_end AS (SELECT DISTINCT ON (date_trunc('month', valuation_date)) date_trunc('month', valuation_date)::date AS month, nw, contrib FROM daily ORDER BY date_trunc('month', valuation_date), valuation_date DESC), deltas AS (SELECT month, nw, contrib, lag(nw) OVER (ORDER BY month) AS prev_nw, lag(contrib) OVER (ORDER BY month) AS prev_contrib FROM month_end) SELECT month::timestamp AS time, ROUND((contrib - prev_contrib)::numeric, 0) AS contributions, ROUND(((nw - prev_nw) - (contrib - prev_contrib))::numeric, 0) AS market_gain FROM deltas WHERE prev_nw IS NOT NULL ORDER BY month"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2626,14 +2626,13 @@
|
|||
"unit": "currencyGBP",
|
||||
"decimals": 0,
|
||||
"custom": {
|
||||
"drawStyle": "bars",
|
||||
"barAlignment": 0,
|
||||
"lineWidth": 1,
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"drawStyle": "line",
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"pointSize": 5,
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"showPoints": "auto",
|
||||
"spanNulls": true,
|
||||
"axisPlacement": "auto",
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
|
|
@ -2743,14 +2742,13 @@
|
|||
"unit": "currencyGBP",
|
||||
"decimals": 0,
|
||||
"custom": {
|
||||
"drawStyle": "bars",
|
||||
"barAlignment": 0,
|
||||
"lineWidth": 1,
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"drawStyle": "line",
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"pointSize": 5,
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"showPoints": "auto",
|
||||
"spanNulls": true,
|
||||
"axisPlacement": "auto",
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
|
|
|
|||
|
|
@ -799,3 +799,120 @@ sys.exit(0 if correct else 1)
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
# ServiceAccount + RBAC for the ingress-dns-sync CronJob to list ingresses cluster-wide.
|
||||
resource "kubernetes_service_account" "ingress_dns_sync" {
|
||||
metadata {
|
||||
name = "ingress-dns-sync"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role" "ingress_dns_sync" {
|
||||
metadata {
|
||||
name = "ingress-dns-sync"
|
||||
}
|
||||
rule {
|
||||
api_groups = ["networking.k8s.io"]
|
||||
resources = ["ingresses"]
|
||||
verbs = ["list"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding" "ingress_dns_sync" {
|
||||
metadata {
|
||||
name = "ingress-dns-sync"
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role.ingress_dns_sync.metadata[0].name
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account.ingress_dns_sync.metadata[0].name
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
# CronJob to sync K8s Ingress hosts -> Technitium CNAME records.
|
||||
# Discovers all *.viktorbarzin.me ingress hosts, ensures each has a CNAME
|
||||
# pointing to viktorbarzin.me in Technitium's authoritative zone.
|
||||
# Prevents the desync where Cloudflare has the record but internal DNS doesn't.
|
||||
resource "kubernetes_cron_job_v1" "technitium_ingress_dns_sync" {
|
||||
metadata {
|
||||
name = "technitium-ingress-dns-sync"
|
||||
namespace = kubernetes_namespace.technitium.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
schedule = "0 * * * *"
|
||||
successful_jobs_history_limit = 1
|
||||
failed_jobs_history_limit = 3
|
||||
job_template {
|
||||
metadata {}
|
||||
spec {
|
||||
template {
|
||||
metadata {}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.ingress_dns_sync.metadata[0].name
|
||||
container {
|
||||
name = "sync"
|
||||
image = "bitnami/kubectl:latest"
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "10m"
|
||||
memory = "32Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "64Mi"
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "TECH_USER"
|
||||
value = var.technitium_username
|
||||
}
|
||||
env {
|
||||
name = "TECH_PASS"
|
||||
value = var.technitium_password
|
||||
}
|
||||
command = ["/bin/sh", "-c", <<-EOT
|
||||
set -e
|
||||
ZONE="viktorbarzin.me"
|
||||
TECH_API="http://technitium-web:5380"
|
||||
|
||||
TOKEN=$$(curl -sf "$$TECH_API/api/user/login?user=$$TECH_USER&pass=$$TECH_PASS" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -z "$$TOKEN" ]; then echo "Login failed"; exit 1; fi
|
||||
|
||||
EXISTING=$$(curl -sf "$$TECH_API/api/zones/records/get?token=$$TOKEN&zone=$$ZONE&domain=$$ZONE&listZone=true" | grep -o '"name":"[^"]*\.viktorbarzin\.me"' | sed 's/"name":"//;s/"//' | sort -u)
|
||||
|
||||
HOSTS=$$(kubectl get ingress -A -o jsonpath='{range .items[*]}{range .spec.rules[*]}{.host}{"\n"}{end}{end}' | grep "\.$$ZONE$$" | grep -v "^$$ZONE$$" | sort -u)
|
||||
|
||||
CREATED=0
|
||||
for HOST in $$HOSTS; do
|
||||
if echo "$$EXISTING" | grep -qx "$$HOST"; then
|
||||
continue
|
||||
fi
|
||||
RESULT=$$(curl -sf "$$TECH_API/api/zones/records/add?token=$$TOKEN&zone=$$ZONE&domain=$$HOST&type=CNAME&cname=$$ZONE&ttl=86400" 2>&1) || true
|
||||
if echo "$$RESULT" | grep -q '"status":"ok"'; then
|
||||
echo "Created CNAME: $$HOST -> $$ZONE"
|
||||
CREATED=$$((CREATED + 1))
|
||||
elif echo "$$RESULT" | grep -q 'already exists'; then
|
||||
echo "Already exists: $$HOST"
|
||||
else
|
||||
echo "Failed: $$HOST -- $$RESULT"
|
||||
fi
|
||||
done
|
||||
echo "Sync complete. Created $$CREATED new records."
|
||||
EOT
|
||||
]
|
||||
}
|
||||
restart_policy = "OnFailure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,13 @@ resource "kubernetes_deployment" "wealthfolio" {
|
|||
metadata[0].annotations["keel.sh/policy"],
|
||||
metadata[0].annotations["keel.sh/trigger"],
|
||||
metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2
|
||||
metadata[0].annotations["keel.sh/match-tag"],
|
||||
spec[0].template[0].spec[0].container[0].image, # KEEL_IGNORE_IMAGE — Keel manages tag updates
|
||||
spec[0].template[0].spec[0].container[1].image,
|
||||
spec[0].template[0].spec[0].container[2].image,
|
||||
metadata[0].annotations["kubernetes.io/change-cause"],
|
||||
metadata[0].annotations["deployment.kubernetes.io/revision"],
|
||||
spec[0].template[0].metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1
|
||||
]
|
||||
}
|
||||
metadata {
|
||||
|
|
@ -422,22 +429,53 @@ resource "kubernetes_deployment" "wealthfolio" {
|
|||
FROM activities
|
||||
WHERE notes LIKE 'cash-flow-match:%'
|
||||
AND account_id = '72d34e09-c1a6-41aa-99ea-abe3305ecc4a' -- Schwab
|
||||
),
|
||||
base AS (
|
||||
SELECT
|
||||
d.id, d.account_id, d.valuation_date, d.account_currency,
|
||||
d.base_currency, d.fx_rate_to_base, d.cash_balance,
|
||||
d.investment_market_value, d.total_value, d.cost_basis,
|
||||
d.net_contribution AS nc_raw,
|
||||
COALESCE(SUM(s.synthetic_net), 0) AS synthetic_adjustment
|
||||
FROM daily_account_valuation d
|
||||
LEFT JOIN synthetic_flows s
|
||||
ON s.account_id = d.account_id
|
||||
AND s.effective_date <= d.valuation_date
|
||||
GROUP BY d.id, d.account_id, d.valuation_date, d.account_currency,
|
||||
d.base_currency, d.fx_rate_to_base, d.cash_balance,
|
||||
d.investment_market_value, d.total_value, d.cost_basis,
|
||||
d.net_contribution
|
||||
),
|
||||
-- LOCF gap-fill: a Fidelity pension valuation of 0 is always a
|
||||
-- PlanViewer scrape gap (the pot can't really be £0), never a real
|
||||
-- balance. Without this a missed scrape craters net worth to £0 for
|
||||
-- the gap and the "Monthly contributions" panel shows a phantom
|
||||
-- withdrawal then rebound (witnessed Feb 2026: -£97k / +£100k). Carry
|
||||
-- the last non-zero day forward across the gap. Scoped to Fidelity
|
||||
-- (account_id below); brokerage 0s are left untouched.
|
||||
filled AS (
|
||||
SELECT *,
|
||||
SUM(CASE WHEN total_value > 0 THEN 1 ELSE 0 END)
|
||||
OVER (PARTITION BY account_id ORDER BY valuation_date) AS tv_grp
|
||||
FROM base
|
||||
)
|
||||
SELECT
|
||||
d.id, d.account_id, d.valuation_date, d.account_currency,
|
||||
d.base_currency, d.fx_rate_to_base, d.cash_balance,
|
||||
d.investment_market_value, d.total_value, d.cost_basis,
|
||||
d.net_contribution AS net_contribution_raw,
|
||||
(d.net_contribution - COALESCE(SUM(s.synthetic_net), 0)) AS net_contribution,
|
||||
COALESCE(SUM(s.synthetic_net), 0) AS synthetic_adjustment
|
||||
FROM daily_account_valuation d
|
||||
LEFT JOIN synthetic_flows s
|
||||
ON s.account_id = d.account_id
|
||||
AND s.effective_date <= d.valuation_date
|
||||
GROUP BY d.id, d.account_id, d.valuation_date, d.account_currency,
|
||||
d.base_currency, d.fx_rate_to_base, d.cash_balance,
|
||||
d.investment_market_value, d.total_value, d.cost_basis,
|
||||
d.net_contribution;
|
||||
id, account_id, valuation_date, account_currency, base_currency,
|
||||
fx_rate_to_base,
|
||||
CASE WHEN account_id = 'a7d6208d-2bd6-4f85-bf54-b77984c78234' AND total_value = 0
|
||||
THEN MAX(cash_balance) OVER w ELSE cash_balance END AS cash_balance,
|
||||
CASE WHEN account_id = 'a7d6208d-2bd6-4f85-bf54-b77984c78234' AND total_value = 0
|
||||
THEN MAX(investment_market_value) OVER w ELSE investment_market_value END AS investment_market_value,
|
||||
CASE WHEN account_id = 'a7d6208d-2bd6-4f85-bf54-b77984c78234' AND total_value = 0
|
||||
THEN MAX(total_value) OVER w ELSE total_value END AS total_value,
|
||||
CASE WHEN account_id = 'a7d6208d-2bd6-4f85-bf54-b77984c78234' AND total_value = 0
|
||||
THEN MAX(cost_basis) OVER w ELSE cost_basis END AS cost_basis,
|
||||
nc_raw AS net_contribution_raw,
|
||||
(CASE WHEN account_id = 'a7d6208d-2bd6-4f85-bf54-b77984c78234' AND total_value = 0
|
||||
THEN MAX(nc_raw) OVER w ELSE nc_raw END) - synthetic_adjustment AS net_contribution,
|
||||
synthetic_adjustment
|
||||
FROM filled
|
||||
WINDOW w AS (PARTITION BY account_id, tv_grp);
|
||||
SQL
|
||||
|
||||
# Snapshot SQLite (online backup — non-blocking).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue