{
  "name": "Shiny Gmail Lightweight Telegram Rules Manager V4.0 Lean",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ]
      },
      "id": "785807e3-d13f-4f66-85f8-106fd7217819",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const update=$input.first()?.json||{};const message=update.message||update;const chat=message.chat||{};const text=String(message.text||'').trim();\nconst allowedChatId=String($env.TELEGRAM_CHAT_ID||'').trim();const chatId=String(chat.id||'').trim();const authorized=Boolean(allowedChatId&&chatId===allowedChatId);\nfunction normPattern(s){return String(s||'').trim().toLowerCase()}function isEmail(s){return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s)}function isDomain(s){return /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(s)&&!s.includes('@')}function extractDomain(s){return String(s||'').toLowerCase().split('@').pop()}function categoryForRuleType(t){t=String(t||'').toLowerCase();if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown'}\nconst parts=text.split(/\\s+/).filter(Boolean);const cmd=(parts[0]||'').toLowerCase().replace(/@\\S+$/,'');const arg=normPattern(parts[1]||'');\nlet action='fallback',ruleType=null,matchType='exact',senderPattern=arg,valid=true,validationError=null,recentCategory=null,recentCount=5,statsDays=7,recentFilter=null,recentSenderPattern=null,recentIsDomain=false,rulesFilter=null;\nconst validCats=new Set(['priority','finances','accounts-subscriptions','account-security','unimportant','needs-review']);\nconst addMap={'/whitelist':'whitelist','/blacklist':'blacklist','/receipt':'force_finance','/review':'force_review','/whitelistdomain':'whitelist','/blacklistdomain':'blacklist','/receiptdomain':'force_finance','/reviewdomain':'force_review'};\nconst removeMap={'/unwhitelist':'whitelist','/unblacklist':'blacklist','/unreceipt':'force_finance','/unreview':'force_review','/unwhitelistdomain':'whitelist','/unblacklistdomain':'blacklist','/unreceiptdomain':'force_finance','/unreviewdomain':'force_review'};\nif(!authorized)action='unauthorized';\nelse if(cmd==='/help'||cmd==='/start')action='help';\nelse if(cmd==='/rules'){action='rules';rulesFilter=arg||null;}\nelse if(cmd==='/stats'){action='stats';const n=Number(arg||7);statsDays=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),90):7;}\nelse if(cmd==='/recent'){\n action='recent';\n if(arg==='failed'){recentFilter='status';recentCount=Number.isFinite(Number(parts[2]))?Math.min(Math.max(Math.trunc(Number(parts[2])),1),20):5;}\n else if(arg==='warnings'||arg==='warned'){recentFilter='warnings';recentCount=Number.isFinite(Number(parts[2]))?Math.min(Math.max(Math.trunc(Number(parts[2])),1),20):5;}\n else if(validCats.has(arg)){recentCategory=arg;const n=Number(normPattern(parts[2]||'5'));recentCount=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),20):5;}\n else if(isEmail(arg)||isDomain(arg)){recentFilter='sender';recentSenderPattern=arg;recentIsDomain=isDomain(arg)&&!isEmail(arg);const n=Number(normPattern(parts[2]||'5'));recentCount=Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),1),20):5;}\n else if(arg&&!Number.isNaN(Number(arg))){recentCount=Math.min(Math.max(Math.trunc(Number(arg)),1),20);}else if(arg){valid=false;validationError='For /recent, use /recent, /recent failed, /recent warnings, /recent 10, /recent needs-review, /recent sender@example.com, or /recent example.com 10.';}\n}\nelse if(cmd==='/explain'){action='explain';if(!arg){valid=false;validationError='Please provide an email or domain.'}else if(!(isEmail(arg)||isDomain(arg))){valid=false;validationError='Please provide a valid email or domain.'}}\nelse if(addMap[cmd]){action='add_rule';ruleType=addMap[cmd];matchType=cmd.endsWith('domain')?'domain':'exact';if(matchType==='exact'&&!isEmail(arg)){valid=false;validationError='Please provide a valid email address.'}if(matchType==='domain'&&!isDomain(arg)){valid=false;validationError='Please provide a valid domain, for example example.com.'}}\nelse if(removeMap[cmd]){action='remove_rule';ruleType=removeMap[cmd];matchType=cmd.endsWith('domain')?'domain':'exact';if(matchType==='exact'&&!isEmail(arg)){valid=false;validationError='Please provide a valid email address.'}if(matchType==='domain'&&!isDomain(arg)){valid=false;validationError='Please provide a valid domain, for example example.com.'}}\nconst explainDomain=isEmail(arg)?extractDomain(arg):arg;return[{json:{authorized,chatId,text,cmd,arg,action,valid,validationError,account:$env.GMAIL_ACCOUNT||'primary',senderPattern,matchType,ruleType,categoryLabel:categoryForRuleType(ruleType),explainInput:arg,explainDomain,explainInputType:isEmail(arg)?'email':(isDomain(arg)?'domain':'unknown'),recentCategory,recentCount,recentFilter,recentSenderPattern,recentIsDomain,statsDays,rulesFilter}}];"
      },
      "id": "11d872bb-5d26-460f-95c3-6d967c248ab6",
      "name": "Parse And Authorize Command",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "help",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "help",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "unauthorized",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "unauthorized",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "invalid",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ !$json.valid }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "add_rule",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "add_rule",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "remove_rule",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "remove_rule",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "rules",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "rules",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "stats",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "stats",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "recent",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "recent",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            },
            {
              "outputKey": "explain",
              "renameOutput": true,
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "explain",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "fallbackOutputName": "unknown_command"
        }
      },
      "id": "b6ab98eb-6a8b-443a-959d-64a7a1f29a36",
      "name": "Route Command",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const j=$input.first().json;let text='';\nfunction dist(a,b){const d=Array.from({length:a.length+1},(_,i)=>[i]);for(let j=1;j<=b.length;j++)d[0][j]=j;for(let i=1;i<=a.length;i++)for(let j=1;j<=b.length;j++)d[i][j]=Math.min(d[i-1][j]+1,d[i][j-1]+1,d[i-1][j-1]+(a[i-1]===b[j-1]?0:1));return d[a.length][b.length];}\nconst known=['/help','/rules','/stats','/recent','/explain','/whitelist','/blacklist','/receipt','/review','/whitelistdomain','/blacklistdomain','/receiptdomain','/reviewdomain','/unwhitelist','/unblacklist','/unreceipt','/unreview','/unwhitelistdomain','/unblacklistdomain','/unreceiptdomain','/unreviewdomain'];\nif(j.action==='unauthorized')text='Command unavailable.';else if(!j.valid)text=`\u26a0\ufe0f ${h(j.validationError)}\\n\\nSend /help for commands.`;else if(j.action==='help')text=`\ud83e\uddf9 <b>Shiny Gmail Manager V4.0</b>\\n\\n<b>Labels</b>\\n\ud83d\udd34 priority = real human conversations\\n\ud83d\udcb0 finances = money, receipts, billing, loans, shopping\\n\ud83d\udd14 accounts-subscriptions = services/tools/automation notices\\n\ud83d\udd10 account-security = OTPs, login, password reset, account safety\\n\ud83d\uddd1 unimportant = clutter\\n\ud83d\udd0e needs-review = automation uncertainty\\n\\n<b>Add sender rules</b>\\n<code>/whitelist boss@example.com</code> \u2192 priority\\n<code>/blacklist promo@example.com</code> \u2192 unimportant\\n<code>/receipt billing@example.com</code> \u2192 finances\\n<code>/review unknown@example.com</code> \u2192 needs-review\\n\\n<b>Add domain rules</b>\\n<code>/whitelistdomain company.com</code>\\n<code>/blacklistdomain spamdomain.com</code>\\n<code>/receiptdomain billingvendor.com</code>\\n<code>/reviewdomain ambiguousvendor.com</code>\\n\\n<b>Remove rules</b>\\n<code>/unwhitelist boss@example.com</code>\\n<code>/unblacklist promo@example.com</code>\\n<code>/unreceipt billing@example.com</code>\\n<code>/unreview unknown@example.com</code>\\n<code>/unwhitelistdomain company.com</code>\\n<code>/unblacklistdomain spamdomain.com</code>\\n<code>/unreceiptdomain billingvendor.com</code>\\n<code>/unreviewdomain ambiguousvendor.com</code>\\n\\n<b>Inspect</b>\\n<code>/rules [finances|priority|domain|exact|disabled|all|pattern]</code>\\n<code>/stats [days]</code>\\n<code>/recent</code>\\n<code>/recent failed</code>\\n<code>/recent warnings</code>\\n<code>/recent needs-review 10</code>\\n<code>/recent sender@example.com 5</code>\\n<code>/recent example.com 10</code>\\n<code>/explain sender@example.com</code>`;else{const typed=j.cmd||'';const best=known.map(c=>[c,dist(typed,c)]).sort((a,b)=>a[1]-b[1])[0];text=best&&best[1]<=3?`Unknown command: ${h(typed)}\\n\\nDid you mean: ${h(best[0])}?\\n\\nSend /help for all commands.`:'Send /help for commands.';}return[{json:{chatId:j.chatId||$env.TELEGRAM_CHAT_ID,text:clampTelegram(text)}}];"
      },
      "id": "f28bd1e7-b0bd-4ed8-97a8-c85964453957",
      "name": "Format Simple Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        -260
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),pattern=String($json.senderPattern).replace(/'/g,\"''\"),matchType=String($json.matchType).replace(/'/g,\"''\"),ruleType=String($json.ruleType).replace(/'/g,\"''\");return \"WITH disabled AS (UPDATE gmail_sender_rules SET enabled=false, updated_at=NOW() WHERE account='\"+account+\"' AND sender_pattern='\"+pattern+\"' AND match_type='\"+matchType+\"' AND rule_type <> '\"+ruleType+\"' AND enabled=true RETURNING rule_type AS superseded_rule_type), upserted AS (INSERT INTO gmail_sender_rules (account, sender_pattern, match_type, rule_type, priority, enabled, created_at, updated_at) VALUES ('\"+account+\"','\"+pattern+\"','\"+matchType+\"','\"+ruleType+\"',10,true,NOW(),NOW()) ON CONFLICT (account, sender_pattern, match_type, rule_type) DO UPDATE SET enabled=true, updated_at=NOW() RETURNING sender_pattern, match_type, rule_type) SELECT u.sender_pattern, u.match_type, u.rule_type, (SELECT json_agg(disabled.superseded_rule_type) FROM disabled) AS superseded_rule_types FROM upserted u;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "33376884-286c-4fca-b603-2e539b12a719",
      "name": "Upsert Rule",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        -20
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{ \"UPDATE gmail_sender_rules SET enabled=false, updated_at=NOW() WHERE account='\"+String($json.account).replace(/'/g,\"''\")+\"' AND sender_pattern='\"+String($json.senderPattern).replace(/'/g,\"''\")+\"' AND match_type='\"+String($json.matchType).replace(/'/g,\"''\")+\"' AND rule_type='\"+String($json.ruleType).replace(/'/g,\"''\")+\"' RETURNING sender_pattern, match_type, rule_type;\" }}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "b0a1202b-6621-4d6c-a975-ec20d3ccb0eb",
      "name": "Disable Rule",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        120
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const row=$input.first()?.json||{};if(row.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Rule change was not saved.\\n\\nDatabase error. No rule was changed.\\n${String(row.error.message||'Try again later.').slice(0,200)}`)}}];const changed=Boolean(row.sender_pattern);const label=original.categoryLabel||mapRule(row.rule_type);let text=original.action==='add_rule'?`\u2705 Rule saved\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`:changed?`\ud83d\uddd1\ufe0f Rule removed\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`:`\u2139\ufe0f Rule not found\\n\\n${original.senderPattern} (${original.matchType}) \u2192 ${emojiCat(label)}`;const superseded=row.superseded_rule_types;if(original.action==='add_rule'&&Array.isArray(superseded)&&superseded.length)text+=`\\nSuperseded previous rule(s): ${superseded.map(t=>emojiCat(mapRule(t))).join(', ')}`;return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "3a0615c4-8d36-4133-a144-ee8cbd00e6ab",
      "name": "Format Rule Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        40
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),f=String($json.rulesFilter||'').replace(/'/g,\"''\");const like=f.replace(/[\\\\%_]/g,m=>'\\\\'+m);const catMap={priority:[\"whitelist\"],unimportant:[\"blacklist\"],finances:[\"force_finance\",\"finance\",\"receipt\",\"force_receipts\"],\"needs-review\":[\"force_review\",\"review\"]};let where=\" WHERE account='\"+account+\"'\";if(f==='disabled')where+=\" AND enabled=false\";else if(f==='all')where+=\"\";else where+=\" AND enabled=true\";if(f&&f!=='disabled'&&f!=='all'){if(f==='domain'||f==='exact')where+=\" AND match_type='\"+f+\"'\";else if(catMap[f])where+=\" AND rule_type IN (\"+catMap[f].map(x=>\"'\"+x+\"'\").join(',')+\")\";else where+=\" AND sender_pattern ILIKE '%\"+like+\"%' ESCAPE '\\\\'\";}return \"SELECT sender_pattern, match_type, rule_type, priority, enabled FROM gmail_sender_rules\"+where+\" ORDER BY enabled DESC, priority ASC, CASE WHEN match_type='exact' THEN 0 ELSE 1 END, created_at DESC, sender_pattern LIMIT 100;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "ffb2cfd2-ea44-421c-ac95-c1c7cbd44b6b",
      "name": "Query Rules",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        260
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load rules safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json).filter(r=>r.sender_pattern);const exact=rows.filter(r=>r.match_type==='exact'),domains=rows.filter(r=>r.match_type==='domain');const typeCounts={};for(const r of rows){const cat=mapRule(r.rule_type);typeCounts[cat]=(typeCounts[cat]||0)+1;}const comp=Object.entries(typeCounts).map(([cat,n])=>`${emojiCat(cat)}: ${n}`).join(' \u00b7 ');function line(r){const disabled=r.enabled===false||r.enabled==='false';return`\u2022 ${disabled?'\u23f8\ufe0f ':''}${h(r.sender_pattern)} \u2192 ${emojiCat(mapRule(r.rule_type))} \u00b7 p${h(r.priority??10)}${disabled?' (disabled)':''}`;}let text=`\ud83d\udccb <b>Your Gmail Rules</b>${original.rulesFilter?` \u2014 ${h(original.rulesFilter)}`:''}\\nExact senders: ${exact.length} \u00b7 Domains: ${domains.length}`;if(comp)text+=`\\n${h(comp)}`;if(!rows.length)text+='\\n\\nNo rules found for this filter. Send /help for examples.';else{if(exact.length)text+='\\n\\n<b>Exact senders</b>\\n'+exact.map(line).join('\\n');if(domains.length)text+='\\n\\n<b>Domains</b>\\n'+domains.map(line).join('\\n');text+='\\n\\n<i>Rule order: lower priority wins; exact beats domain at same priority; ties use newest first.</i>';}return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "51ca239b-c2b7-41b3-b0ae-26ab741fd3f9",
      "name": "Format Rules Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        260
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),days=Math.min(Math.max(Number($json.statsDays||7),1),90);return \"WITH recent AS (SELECT category, source, sender, updated_at, confidence FROM gmail_message_ledger WHERE account='\"+account+\"' AND status='completed' AND updated_at >= NOW() - INTERVAL '\"+days+\" days'), totals AS (SELECT COUNT(*) AS total FROM recent), cats AS (SELECT category, COUNT(*) AS count FROM recent GROUP BY category), sources AS (SELECT source, COUNT(*) AS count FROM recent GROUP BY source), senders AS (SELECT sender, COUNT(*) AS count FROM recent WHERE sender <> '' GROUP BY sender ORDER BY count DESC LIMIT 5), rule_hits AS (SELECT COUNT(*) FILTER (WHERE source NOT IN ('ai','ai_low_confidence','ai_failed')) AS rule_count, COUNT(*) AS total FROM recent), ai_cats AS (SELECT category, COUNT(*) AS count FROM recent WHERE source IN ('ai','ai_low_confidence','ai_failed') GROUP BY category ORDER BY count DESC LIMIT 5), ai_confidence AS (SELECT COALESCE(ROUND(AVG(confidence)::numeric,2),0) AS avg_conf, COALESCE(ROUND(MIN(confidence)::numeric,2),0) AS min_conf, COUNT(*) AS ai_count FROM recent WHERE source IN ('ai','ai_low_confidence')), daily_avg AS (SELECT COALESCE(ROUND(COUNT(*)::numeric / GREATEST(EXTRACT(EPOCH FROM (MAX(updated_at) - MIN(updated_at))) / 86400.0, 1), 1),0) AS avg_per_day, COALESCE(GREATEST(CEIL(EXTRACT(EPOCH FROM (MAX(updated_at) - MIN(updated_at))) / 86400.0), 1)::int,1) AS actual_days FROM recent), review_senders AS (SELECT sender, COUNT(*) AS count FROM recent WHERE category='needs-review' AND sender <> '' GROUP BY sender HAVING COUNT(*) >= 2 ORDER BY count DESC LIMIT 3) SELECT json_build_object('days',\"+days+\",'total',(SELECT total FROM totals),'avg_per_day',(SELECT avg_per_day FROM daily_avg),'actual_days',(SELECT actual_days FROM daily_avg),'categories',(SELECT COALESCE(json_agg(cats),'[]'::json) FROM cats),'sources',(SELECT COALESCE(json_agg(sources),'[]'::json) FROM sources),'top_senders',(SELECT COALESCE(json_agg(senders),'[]'::json) FROM senders),'rule_hits',(SELECT row_to_json(rule_hits) FROM rule_hits),'ai_categories',(SELECT COALESCE(json_agg(ai_cats),'[]'::json) FROM ai_cats),'ai_confidence',(SELECT row_to_json(ai_confidence) FROM ai_confidence),'review_senders',(SELECT COALESCE(json_agg(review_senders),'[]'::json) FROM review_senders)) AS stats;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "42654a75-7d9e-44a8-9637-a440c96e66d6",
      "name": "Query Stats",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        420
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load stats safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const data=first.stats||{};function list(arr,key='category',emoji=false){return Array.isArray(arr)&&arr.length?arr.map(x=>`${emoji?emojiCat(x[key]):(x[key]||x.source||x.sender)}: ${x.count}`).join(' | '):'none';}const total=Number(data.total||0),rh=data.rule_hits||{},ruleCount=Number(rh.rule_count||0),pct=total?Math.round(ruleCount/total*100):0,ai=data.ai_confidence||{};let text=`\ud83d\udcca <b>Shiny Gmail Stats</b> \u2014 last ${data.days||original.statsDays||7} day(s)\\n\\nTotal processed: ${total}\\nDaily average: ${data.avg_per_day||0} emails/day (over ${data.actual_days||1} actual day(s))\\nCategories: ${list(data.categories||[],'category',true)}\\nSources: ${list(data.sources||[],'source',false)}\\nAI bypass rate: ${pct}% (${ruleCount}/${total} bypassed AI)\\n\ud83e\udd16 AI confidence: avg ${ai.avg_conf??0} \u00b7 min ${ai.min_conf??0} \u00b7 across ${ai.ai_count||0} classification(s)\\nTop AI-classified: ${list(data.ai_categories||[],'category',true)}\\nTop senders: ${list(data.top_senders||[],'sender',false)}`;const rs=data.review_senders||[];if(Array.isArray(rs)&&rs.length)text+=`\\n\\n\ud83d\udca1 <b>Rule candidates</b> (frequent needs-review)\\n`+rs.map(s=>`\u2022 ${h(s.sender)} (${s.count}\u00d7 \u2192 /whitelist, /receipt, or /blacklist)`).join('\\n');return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "03ae3625-29e7-4a96-9057-d623d06d5083",
      "name": "Format Stats Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        420
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),limit=Math.min(Math.max(Number($json.recentCount||5),1),20),cat=$json.recentCategory?String($json.recentCategory).replace(/'/g,\"''\"):null,senderPattern=$json.recentSenderPattern?String($json.recentSenderPattern).replace(/'/g,\"''\"):null,isDomain=Boolean($json.recentIsDomain),failed=$json.recentFilter==='status',warned=$json.recentFilter==='warnings';let where=\" WHERE account='\"+account+\"'\";if(warned)where+=\" AND status LIKE 'completed%' AND error_message IS NOT NULL\";else where+=\" AND status='\"+(failed?'failed':'completed')+\"'\";if(cat)where+=\" AND category='\"+cat+\"'\";if(senderPattern)where+=isDomain?\" AND (LOWER(split_part(sender,'@',2))='\"+senderPattern+\"' OR LOWER(split_part(sender,'@',2)) LIKE '%.\"+senderPattern+\"')\":\" AND sender='\"+senderPattern+\"'\";return \"SELECT gmail_message_id, sender, subject, category, source, confidence, reasoning, status, error_message, updated_at FROM gmail_message_ledger\"+where+\" ORDER BY updated_at DESC LIMIT \"+limit+\";\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "88541d85-c0e3-473e-994b-ff2dc78383b8",
      "name": "Query Recent",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        580
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not load recent classifications safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json).filter(r=>r.gmail_message_id);const failed=original.recentFilter==='status';const warned=original.recentFilter==='warnings';const filter=warned?' \u2014 \u26a0\ufe0f warnings':(failed?' \u2014 \ud83d\udea8 failed':(original.recentCategory?` \u2014 ${emojiCat(original.recentCategory)}`:(original.recentSenderPattern?` \u2014 ${h(original.recentSenderPattern)}`:'')));function gmailLink(id){return id&&!String(id).startsWith('input_failure_')?`https://mail.google.com/mail/u/0/#all/${encodeURIComponent(id)}`:null;}let text=`\ud83d\udcec <b>Recent Classifications${filter}</b>\\n`;if(!rows.length)text+='\\nNo matching emails found yet.';else text+='\\n'+rows.map((r,i)=>{const link=gmailLink(r.gmail_message_id);const subj=h(r.subject||'(no subject)');const subject=link?`<a href=\"${link}\">${subj}</a>`:subj;if(warned)return`${i+1}. <b>\u26a0\ufe0f warning</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'completed_with_warning')} \u2014 ${h(r.error_message||'Completed with warning.')}`;if(failed)return`${i+1}. <b>\ud83d\udea8 failed</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'failed')} \u2014 ${h(r.error_message||r.reasoning||'No error message.')}\\nNext: rerun tomorrow, or inspect Gmail label state if this repeats.`;return`${i+1}. <b>${emojiCat(r.category)}</b> \u2014 ${h(r.sender||'unknown')} \u00b7 ${relativeTime(r.updated_at)}\\n${subject}\\n${h(r.source||'unknown')} \u00b7 confidence ${h(r.confidence??0)}\\n${h(r.reasoning||'')}`}).join('\\n\\n');return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "e211964c-3db9-4faa-aa10-0b1a0d3bb71c",
      "name": "Format Recent Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        580
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{(()=>{const account=String($json.account).replace(/'/g,\"''\"),input=String($json.explainInput).replace(/'/g,\"''\"),domain=String($json.explainDomain).replace(/'/g,\"''\");return \"WITH rule_matches AS (SELECT sender_pattern, match_type, rule_type, priority, created_at, CASE WHEN sender_pattern='\"+input+\"' THEN 'exact sender match' WHEN match_type='domain' AND '\"+domain+\"'=sender_pattern THEN 'root domain match' WHEN match_type='domain' AND '\"+domain+\"' LIKE '%.' || sender_pattern THEN 'subdomain match' ELSE 'matched' END AS match_reason, CASE WHEN match_type='exact' AND sender_pattern='\"+input+\"' THEN 0 WHEN match_type='domain' AND '\"+domain+\"'=sender_pattern THEN 1 WHEN match_type='domain' AND '\"+domain+\"' LIKE '%.' || sender_pattern THEN 2 ELSE 9 END AS match_strength, NULL::timestamptz AS last_seen, 0 AS hist_count, 0 AS row_type FROM gmail_sender_rules WHERE account='\"+account+\"' AND enabled=true AND (sender_pattern='\"+input+\"' OR (match_type='domain' AND ('\"+domain+\"'=sender_pattern OR '\"+domain+\"' LIKE '%.' || sender_pattern))) ORDER BY priority ASC, match_strength ASC, created_at DESC LIMIT 5), history AS (SELECT NULL::text AS sender_pattern, NULL::text AS match_type, category AS rule_type, NULL::integer AS priority, MAX(updated_at) AS created_at, ('history: ' || source || ' \u00d7 ' || COUNT(*) || ', conf ' || ROUND(AVG(confidence)::numeric, 2)::text) AS match_reason, 99 AS match_strength, MAX(updated_at) AS last_seen, COUNT(*)::int AS hist_count, 1 AS row_type FROM gmail_message_ledger WHERE account='\"+account+\"' AND status='completed' AND (sender='\"+input+\"' OR LOWER(split_part(sender,'@',2))='\"+domain+\"' OR LOWER(split_part(sender,'@',2)) LIKE '%.\"+domain+\"') GROUP BY category, source ORDER BY COUNT(*) DESC LIMIT 3) SELECT * FROM rule_matches UNION ALL SELECT * FROM history ORDER BY row_type ASC, priority ASC NULLS LAST, match_strength ASC, hist_count DESC, created_at DESC NULLS LAST;\";})()}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "bf53fdcb-44ee-473b-8a0c-45056bcc40dc",
      "name": "Query Explain",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        700,
        740
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}\nfunction clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use a narrower command or DB view for full details.';}\nfunction emojiCat(c){const m={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${m[c]||''} ${c||'unknown'}`.trim();}\nfunction mapRule(t){if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return t||'unknown';}\nfunction relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!Number.isFinite(ms)||ms<0)return'just now';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}const original=$('Parse And Authorize Command').first().json;const first=$input.first()?.json||{};if(first.error)return[{json:{chatId:original.chatId,text:clampTelegram(`\u26a0\ufe0f Database unavailable.\\n\\nCould not preview rule matching safely. Try again later.\\n${String(first.error.message||'').slice(0,200)}`)}}];const rows=$input.all().map(i=>i.json);const ruleRows=rows.filter(r=>r.row_type===0&&r.sender_pattern);const historyRows=rows.filter(r=>r.row_type===1);let decision='Gemini will classify using metadata';let text=`\ud83d\udd0e <b>Classification Preview</b>\\n\\nInput: ${h(original.explainInput)}\\n`;if(ruleRows.length){const w=ruleRows[0];decision=`${emojiCat(mapRule(w.rule_type))} via ${w.match_type==='domain'?'domain rule':'exact sender rule'}`;}text+=`<b>Decision today:</b> ${h(decision)}\\n<i>Winner selected by: priority ASC; exact beats domain at same priority; newest rule wins remaining ties.</i>\\n`;if(!ruleRows.length)text+='\\nNo sender rule matches. Gemini will classify using metadata.';else{const w=ruleRows[0];text+=`\\n<b>Winner</b>\\n${h(w.sender_pattern)} (${h(w.match_type)})\\nMatch: ${h(w.match_reason||'matched')}\\nPriority: p${h(w.priority??10)}\\nRule type: ${h(w.rule_type)}\\nFuture category: ${emojiCat(mapRule(w.rule_type))}`;if(ruleRows.length>1){const runner=ruleRows[1];let reason='More recently created rule at same priority';if(Number(w.priority)!==Number(runner.priority))reason=`Lower priority value (p${w.priority} before p${runner.priority})`;else if(w.match_type!==runner.match_type)reason='Exact match beats domain match at same priority';else if(Number(w.match_strength)!==Number(runner.match_strength))reason='More specific domain match wins before broader subdomain/root matches';text+=`\\n\\n<b>Also matched</b>\\n`+ruleRows.slice(1).map((r,i)=>`${i+2}. ${h(r.sender_pattern)} (${h(r.match_type)}) \u00b7 ${h(r.match_reason||'matched')} \u00b7 p${h(r.priority??10)} \u2192 ${emojiCat(mapRule(r.rule_type))}`).join('\\n')+`\\n\u26a1 Resolution: ${h(reason)}`;}}\nif(historyRows.length){text+=`\\n\\n<b>Classification history</b>\\n`+historyRows.map(r=>`${emojiCat(r.rule_type)} \u00b7 ${h(r.match_reason)} \u00b7 last ${relativeTime(r.last_seen)}`).join('\\n');if(ruleRows.length){const future=mapRule(ruleRows[0].rule_type);const dominant=historyRows[0].rule_type;if(dominant&&future&&dominant!==future)text+=`\\n\\n\u26a0\ufe0f <b>Drift note</b>\\nCurrent winning rule classifies future mail as ${emojiCat(future)}, but recent history is mostly ${emojiCat(dominant)}.`;}}else text+='\\n\\nNo classification history found for this sender/domain.';return[{json:{chatId:original.chatId,text:clampTelegram(text)}}];"
      },
      "id": "4062fd69-0c08-4b74-a250-a3bd738c1267",
      "name": "Format Explain Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        740
      ]
    },
    {
      "parameters": {
        "operation": "sendMessage",
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "a63972b3-09d3-42ee-8b6b-284347167e05",
      "name": "Send Telegram Reply",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1220,
        260
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Parse And Authorize Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse And Authorize Command": {
      "main": [
        [
          {
            "node": "Route Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Command": {
      "main": [
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upsert Rule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Disable Rule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Rules",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Stats",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Recent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Query Explain",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Simple Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Rule": {
      "main": [
        [
          {
            "node": "Format Rule Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Disable Rule": {
      "main": [
        [
          {
            "node": "Format Rule Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Rule Result": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Rules": {
      "main": [
        [
          {
            "node": "Format Rules Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Rules Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Stats": {
      "main": [
        [
          {
            "node": "Format Stats Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Stats Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Recent": {
      "main": [
        [
          {
            "node": "Format Recent Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Recent Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Explain": {
      "main": [
        [
          {
            "node": "Format Explain Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Explain Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Simple Reply": {
      "main": [
        [
          {
            "node": "Send Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "saveExecutionProgress": false,
    "executionTimeout": 300
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 1,
  "versionId": "v4.0-lean-generated"
}