VinOS Agent Claude Opus 4.6 commited on
Commit
f271c11
·
1 Parent(s): 99f2bdb

feat: overhaul Social Autopilot dashboard + per-channel posting

Browse files

- Migrate quota from 20 total to 10 per channel (IG, Threads, X, LinkedIn, FB, Pinterest)
- Add postToChannel() for per-channel posting with correct content mapping
- Fix PostProxy bug: was sending LinkedIn content to X, now uses x_id/x_en
- Fix approve route: per-platform text revision instead of overwriting all fields
- Add new API endpoints: account-stats, quota, analytics/recent/:count, pipeline/post, pipeline/sentiment, download/zip
- Add quickPostPreFlight() to sentiment_agent for MiroFish pre-flight testing
- Complete dashboard redesign: pipeline wizard, variant cards with platform tabs,
per-channel toggles, sentiment badges, zip download, post tracking table,
per-channel quota bars, performance analytics panel
- Install archiver for zip downloads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

package-lock.json CHANGED
@@ -12,6 +12,7 @@
12
  "@huggingface/hub": "^2.11.0",
13
  "@huggingface/inference": "^4.13.15",
14
  "@zernio/node": "^0.2.4",
 
15
  "axios": "^1.6.2",
16
  "cors": "^2.8.5",
17
  "dotenv": "^16.3.1",
@@ -560,6 +561,73 @@
560
  "url": "https://opencollective.com/libvips"
561
  }
562
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  "node_modules/@noble/ciphers": {
564
  "version": "1.3.0",
565
  "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
@@ -584,6 +652,16 @@
584
  "url": "https://paulmillr.com/funding/"
585
  }
586
  },
 
 
 
 
 
 
 
 
 
 
587
  "node_modules/@swc/helpers": {
588
  "version": "0.5.19",
589
  "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
@@ -602,6 +680,18 @@
602
  "node": ">=18"
603
  }
604
  },
 
 
 
 
 
 
 
 
 
 
 
 
605
  "node_modules/accepts": {
606
  "version": "1.3.8",
607
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -629,17 +719,70 @@
629
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
630
  "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
631
  "license": "MIT",
632
- "optional": true,
633
  "engines": {
634
  "node": ">=8"
635
  }
636
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  "node_modules/array-flatten": {
638
  "version": "1.1.1",
639
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
640
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
641
  "license": "MIT"
642
  },
 
 
 
 
 
 
643
  "node_modules/asynckit": {
644
  "version": "0.4.0",
645
  "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -657,6 +800,113 @@
657
  "proxy-from-env": "^1.1.0"
658
  }
659
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  "node_modules/base64-js": {
661
  "version": "1.5.1",
662
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -710,6 +960,15 @@
710
  "npm": "1.2.8000 || >= 1.4.16"
711
  }
712
  },
 
 
 
 
 
 
 
 
 
713
  "node_modules/brotli": {
714
  "version": "1.3.3",
715
  "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
@@ -719,6 +978,39 @@
719
  "base64-js": "^1.1.2"
720
  }
721
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
722
  "node_modules/buffer-equal-constant-time": {
723
  "version": "1.0.1",
724
  "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -800,6 +1092,24 @@
800
  "node": ">=0.8"
801
  }
802
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  "node_modules/combined-stream": {
804
  "version": "1.0.8",
805
  "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -812,6 +1122,22 @@
812
  "node": ">= 0.8"
813
  }
814
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  "node_modules/content-disposition": {
816
  "version": "0.5.4",
817
  "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -848,6 +1174,12 @@
848
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
849
  "license": "MIT"
850
  },
 
 
 
 
 
 
851
  "node_modules/cors": {
852
  "version": "2.8.6",
853
  "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -865,6 +1197,45 @@
865
  "url": "https://opencollective.com/express"
866
  }
867
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  "node_modules/debug": {
869
  "version": "2.6.9",
870
  "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -983,6 +1354,12 @@
983
  "node": ">= 0.4"
984
  }
985
  },
 
 
 
 
 
 
986
  "node_modules/ecdsa-sig-formatter": {
987
  "version": "1.0.11",
988
  "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1002,8 +1379,7 @@
1002
  "version": "8.0.0",
1003
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1004
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1005
- "license": "MIT",
1006
- "optional": true
1007
  },
1008
  "node_modules/encodeurl": {
1009
  "version": "2.0.0",
@@ -1074,6 +1450,33 @@
1074
  "node": ">= 0.6"
1075
  }
1076
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  "node_modules/express": {
1078
  "version": "4.22.1",
1079
  "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -1132,6 +1535,12 @@
1132
  "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1133
  "license": "MIT"
1134
  },
 
 
 
 
 
 
1135
  "node_modules/finalhandler": {
1136
  "version": "1.3.2",
1137
  "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -1187,6 +1596,22 @@
1187
  "unicode-trie": "^2.0.0"
1188
  }
1189
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1190
  "node_modules/form-data": {
1191
  "version": "4.0.5",
1192
  "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -1310,6 +1735,27 @@
1310
  "node": ">= 0.4"
1311
  }
1312
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  "node_modules/google-auth-library": {
1314
  "version": "9.15.1",
1315
  "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
@@ -1391,6 +1837,12 @@
1391
  "url": "https://github.com/sponsors/ljharb"
1392
  }
1393
  },
 
 
 
 
 
 
1394
  "node_modules/gtoken": {
1395
  "version": "7.1.0",
1396
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
@@ -1511,6 +1963,26 @@
1511
  "node": ">=0.10.0"
1512
  }
1513
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  "node_modules/inherits": {
1515
  "version": "2.0.4",
1516
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -1546,7 +2018,6 @@
1546
  "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1547
  "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1548
  "license": "MIT",
1549
- "optional": true,
1550
  "engines": {
1551
  "node": ">=8"
1552
  }
@@ -1596,6 +2067,33 @@
1596
  "url": "https://github.com/sponsors/sindresorhus"
1597
  }
1598
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1599
  "node_modules/js-md5": {
1600
  "version": "0.8.3",
1601
  "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
@@ -1632,6 +2130,48 @@
1632
  "safe-buffer": "^5.0.1"
1633
  }
1634
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1635
  "node_modules/linebreak": {
1636
  "version": "1.1.0",
1637
  "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
@@ -1651,6 +2191,18 @@
1651
  "node": ">= 0.4"
1652
  }
1653
  },
 
 
 
 
 
 
 
 
 
 
 
 
1654
  "node_modules/math-intrinsics": {
1655
  "version": "1.1.0",
1656
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1720,6 +2272,30 @@
1720
  "node": ">= 0.6"
1721
  }
1722
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1723
  "node_modules/ms": {
1724
  "version": "2.0.0",
1725
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1767,6 +2343,15 @@
1767
  }
1768
  }
1769
  },
 
 
 
 
 
 
 
 
 
1770
  "node_modules/object-assign": {
1771
  "version": "4.1.1",
1772
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1818,6 +2403,12 @@
1818
  "url": "https://github.com/sponsors/sindresorhus"
1819
  }
1820
  },
 
 
 
 
 
 
1821
  "node_modules/pako": {
1822
  "version": "0.2.9",
1823
  "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
@@ -1833,6 +2424,31 @@
1833
  "node": ">= 0.8"
1834
  }
1835
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1836
  "node_modules/path-to-regexp": {
1837
  "version": "0.1.12",
1838
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@@ -1858,6 +2474,21 @@
1858
  "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
1859
  "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
1860
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1861
  "node_modules/proxy-addr": {
1862
  "version": "2.0.7",
1863
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1916,6 +2547,43 @@
1916
  "node": ">= 0.8"
1917
  }
1918
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1919
  "node_modules/restructure": {
1920
  "version": "3.0.2",
1921
  "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
@@ -2067,6 +2735,27 @@
2067
  "@img/sharp-win32-x64": "0.34.5"
2068
  }
2069
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2070
  "node_modules/side-channel": {
2071
  "version": "1.1.0",
2072
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -2139,6 +2828,18 @@
2139
  "url": "https://github.com/sponsors/ljharb"
2140
  }
2141
  },
 
 
 
 
 
 
 
 
 
 
 
 
2142
  "node_modules/statuses": {
2143
  "version": "2.0.2",
2144
  "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -2148,12 +2849,46 @@
2148
  "node": ">= 0.8"
2149
  }
2150
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2151
  "node_modules/string-width": {
2152
  "version": "4.2.3",
2153
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2154
  "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2155
  "license": "MIT",
2156
- "optional": true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2157
  "dependencies": {
2158
  "emoji-regex": "^8.0.0",
2159
  "is-fullwidth-code-point": "^3.0.0",
@@ -2168,7 +2903,6 @@
2168
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2169
  "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2170
  "license": "MIT",
2171
- "optional": true,
2172
  "dependencies": {
2173
  "ansi-regex": "^5.0.1"
2174
  },
@@ -2176,6 +2910,49 @@
2176
  "node": ">=8"
2177
  }
2178
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2179
  "node_modules/tiny-inflate": {
2180
  "version": "1.0.3",
2181
  "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -2251,6 +3028,12 @@
2251
  "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
2252
  "license": "BSD"
2253
  },
 
 
 
 
 
 
2254
  "node_modules/utils-merge": {
2255
  "version": "1.0.1",
2256
  "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -2294,6 +3077,121 @@
2294
  "webidl-conversions": "^3.0.0"
2295
  }
2296
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2297
  "node_modules/wsl-utils": {
2298
  "version": "0.1.0",
2299
  "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
@@ -2308,6 +3206,20 @@
2308
  "funding": {
2309
  "url": "https://github.com/sponsors/sindresorhus"
2310
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2311
  }
2312
  }
2313
  }
 
12
  "@huggingface/hub": "^2.11.0",
13
  "@huggingface/inference": "^4.13.15",
14
  "@zernio/node": "^0.2.4",
15
+ "archiver": "^7.0.1",
16
  "axios": "^1.6.2",
17
  "cors": "^2.8.5",
18
  "dotenv": "^16.3.1",
 
561
  "url": "https://opencollective.com/libvips"
562
  }
563
  },
564
+ "node_modules/@isaacs/cliui": {
565
+ "version": "8.0.2",
566
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
567
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
568
+ "license": "ISC",
569
+ "dependencies": {
570
+ "string-width": "^5.1.2",
571
+ "string-width-cjs": "npm:string-width@^4.2.0",
572
+ "strip-ansi": "^7.0.1",
573
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
574
+ "wrap-ansi": "^8.1.0",
575
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
576
+ },
577
+ "engines": {
578
+ "node": ">=12"
579
+ }
580
+ },
581
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
582
+ "version": "6.2.2",
583
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
584
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
585
+ "license": "MIT",
586
+ "engines": {
587
+ "node": ">=12"
588
+ },
589
+ "funding": {
590
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
591
+ }
592
+ },
593
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
594
+ "version": "9.2.2",
595
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
596
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
597
+ "license": "MIT"
598
+ },
599
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
600
+ "version": "5.1.2",
601
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
602
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
603
+ "license": "MIT",
604
+ "dependencies": {
605
+ "eastasianwidth": "^0.2.0",
606
+ "emoji-regex": "^9.2.2",
607
+ "strip-ansi": "^7.0.1"
608
+ },
609
+ "engines": {
610
+ "node": ">=12"
611
+ },
612
+ "funding": {
613
+ "url": "https://github.com/sponsors/sindresorhus"
614
+ }
615
+ },
616
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
617
+ "version": "7.2.0",
618
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
619
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
620
+ "license": "MIT",
621
+ "dependencies": {
622
+ "ansi-regex": "^6.2.2"
623
+ },
624
+ "engines": {
625
+ "node": ">=12"
626
+ },
627
+ "funding": {
628
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
629
+ }
630
+ },
631
  "node_modules/@noble/ciphers": {
632
  "version": "1.3.0",
633
  "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
 
652
  "url": "https://paulmillr.com/funding/"
653
  }
654
  },
655
+ "node_modules/@pkgjs/parseargs": {
656
+ "version": "0.11.0",
657
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
658
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
659
+ "license": "MIT",
660
+ "optional": true,
661
+ "engines": {
662
+ "node": ">=14"
663
+ }
664
+ },
665
  "node_modules/@swc/helpers": {
666
  "version": "0.5.19",
667
  "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
 
680
  "node": ">=18"
681
  }
682
  },
683
+ "node_modules/abort-controller": {
684
+ "version": "3.0.0",
685
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
686
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
687
+ "license": "MIT",
688
+ "dependencies": {
689
+ "event-target-shim": "^5.0.0"
690
+ },
691
+ "engines": {
692
+ "node": ">=6.5"
693
+ }
694
+ },
695
  "node_modules/accepts": {
696
  "version": "1.3.8",
697
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
 
719
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
720
  "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
721
  "license": "MIT",
 
722
  "engines": {
723
  "node": ">=8"
724
  }
725
  },
726
+ "node_modules/ansi-styles": {
727
+ "version": "6.2.3",
728
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
729
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
730
+ "license": "MIT",
731
+ "engines": {
732
+ "node": ">=12"
733
+ },
734
+ "funding": {
735
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
736
+ }
737
+ },
738
+ "node_modules/archiver": {
739
+ "version": "7.0.1",
740
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
741
+ "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
742
+ "license": "MIT",
743
+ "dependencies": {
744
+ "archiver-utils": "^5.0.2",
745
+ "async": "^3.2.4",
746
+ "buffer-crc32": "^1.0.0",
747
+ "readable-stream": "^4.0.0",
748
+ "readdir-glob": "^1.1.2",
749
+ "tar-stream": "^3.0.0",
750
+ "zip-stream": "^6.0.1"
751
+ },
752
+ "engines": {
753
+ "node": ">= 14"
754
+ }
755
+ },
756
+ "node_modules/archiver-utils": {
757
+ "version": "5.0.2",
758
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
759
+ "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
760
+ "license": "MIT",
761
+ "dependencies": {
762
+ "glob": "^10.0.0",
763
+ "graceful-fs": "^4.2.0",
764
+ "is-stream": "^2.0.1",
765
+ "lazystream": "^1.0.0",
766
+ "lodash": "^4.17.15",
767
+ "normalize-path": "^3.0.0",
768
+ "readable-stream": "^4.0.0"
769
+ },
770
+ "engines": {
771
+ "node": ">= 14"
772
+ }
773
+ },
774
  "node_modules/array-flatten": {
775
  "version": "1.1.1",
776
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
777
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
778
  "license": "MIT"
779
  },
780
+ "node_modules/async": {
781
+ "version": "3.2.6",
782
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
783
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
784
+ "license": "MIT"
785
+ },
786
  "node_modules/asynckit": {
787
  "version": "0.4.0",
788
  "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
 
800
  "proxy-from-env": "^1.1.0"
801
  }
802
  },
803
+ "node_modules/b4a": {
804
+ "version": "1.8.0",
805
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
806
+ "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
807
+ "license": "Apache-2.0",
808
+ "peerDependencies": {
809
+ "react-native-b4a": "*"
810
+ },
811
+ "peerDependenciesMeta": {
812
+ "react-native-b4a": {
813
+ "optional": true
814
+ }
815
+ }
816
+ },
817
+ "node_modules/balanced-match": {
818
+ "version": "1.0.2",
819
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
820
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
821
+ "license": "MIT"
822
+ },
823
+ "node_modules/bare-events": {
824
+ "version": "2.8.2",
825
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
826
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
827
+ "license": "Apache-2.0",
828
+ "peerDependencies": {
829
+ "bare-abort-controller": "*"
830
+ },
831
+ "peerDependenciesMeta": {
832
+ "bare-abort-controller": {
833
+ "optional": true
834
+ }
835
+ }
836
+ },
837
+ "node_modules/bare-fs": {
838
+ "version": "4.5.6",
839
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
840
+ "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
841
+ "license": "Apache-2.0",
842
+ "dependencies": {
843
+ "bare-events": "^2.5.4",
844
+ "bare-path": "^3.0.0",
845
+ "bare-stream": "^2.6.4",
846
+ "bare-url": "^2.2.2",
847
+ "fast-fifo": "^1.3.2"
848
+ },
849
+ "engines": {
850
+ "bare": ">=1.16.0"
851
+ },
852
+ "peerDependencies": {
853
+ "bare-buffer": "*"
854
+ },
855
+ "peerDependenciesMeta": {
856
+ "bare-buffer": {
857
+ "optional": true
858
+ }
859
+ }
860
+ },
861
+ "node_modules/bare-os": {
862
+ "version": "3.8.0",
863
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
864
+ "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
865
+ "license": "Apache-2.0",
866
+ "engines": {
867
+ "bare": ">=1.14.0"
868
+ }
869
+ },
870
+ "node_modules/bare-path": {
871
+ "version": "3.0.0",
872
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
873
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
874
+ "license": "Apache-2.0",
875
+ "dependencies": {
876
+ "bare-os": "^3.0.1"
877
+ }
878
+ },
879
+ "node_modules/bare-stream": {
880
+ "version": "2.10.0",
881
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz",
882
+ "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==",
883
+ "license": "Apache-2.0",
884
+ "dependencies": {
885
+ "streamx": "^2.25.0",
886
+ "teex": "^1.0.1"
887
+ },
888
+ "peerDependencies": {
889
+ "bare-buffer": "*",
890
+ "bare-events": "*"
891
+ },
892
+ "peerDependenciesMeta": {
893
+ "bare-buffer": {
894
+ "optional": true
895
+ },
896
+ "bare-events": {
897
+ "optional": true
898
+ }
899
+ }
900
+ },
901
+ "node_modules/bare-url": {
902
+ "version": "2.4.0",
903
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
904
+ "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
905
+ "license": "Apache-2.0",
906
+ "dependencies": {
907
+ "bare-path": "^3.0.0"
908
+ }
909
+ },
910
  "node_modules/base64-js": {
911
  "version": "1.5.1",
912
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
 
960
  "npm": "1.2.8000 || >= 1.4.16"
961
  }
962
  },
963
+ "node_modules/brace-expansion": {
964
+ "version": "2.0.2",
965
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
966
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
967
+ "license": "MIT",
968
+ "dependencies": {
969
+ "balanced-match": "^1.0.0"
970
+ }
971
+ },
972
  "node_modules/brotli": {
973
  "version": "1.3.3",
974
  "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
 
978
  "base64-js": "^1.1.2"
979
  }
980
  },
981
+ "node_modules/buffer": {
982
+ "version": "6.0.3",
983
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
984
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
985
+ "funding": [
986
+ {
987
+ "type": "github",
988
+ "url": "https://github.com/sponsors/feross"
989
+ },
990
+ {
991
+ "type": "patreon",
992
+ "url": "https://www.patreon.com/feross"
993
+ },
994
+ {
995
+ "type": "consulting",
996
+ "url": "https://feross.org/support"
997
+ }
998
+ ],
999
+ "license": "MIT",
1000
+ "dependencies": {
1001
+ "base64-js": "^1.3.1",
1002
+ "ieee754": "^1.2.1"
1003
+ }
1004
+ },
1005
+ "node_modules/buffer-crc32": {
1006
+ "version": "1.0.0",
1007
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
1008
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
1009
+ "license": "MIT",
1010
+ "engines": {
1011
+ "node": ">=8.0.0"
1012
+ }
1013
+ },
1014
  "node_modules/buffer-equal-constant-time": {
1015
  "version": "1.0.1",
1016
  "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
 
1092
  "node": ">=0.8"
1093
  }
1094
  },
1095
+ "node_modules/color-convert": {
1096
+ "version": "2.0.1",
1097
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1098
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1099
+ "license": "MIT",
1100
+ "dependencies": {
1101
+ "color-name": "~1.1.4"
1102
+ },
1103
+ "engines": {
1104
+ "node": ">=7.0.0"
1105
+ }
1106
+ },
1107
+ "node_modules/color-name": {
1108
+ "version": "1.1.4",
1109
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1110
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1111
+ "license": "MIT"
1112
+ },
1113
  "node_modules/combined-stream": {
1114
  "version": "1.0.8",
1115
  "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
 
1122
  "node": ">= 0.8"
1123
  }
1124
  },
1125
+ "node_modules/compress-commons": {
1126
+ "version": "6.0.2",
1127
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
1128
+ "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
1129
+ "license": "MIT",
1130
+ "dependencies": {
1131
+ "crc-32": "^1.2.0",
1132
+ "crc32-stream": "^6.0.0",
1133
+ "is-stream": "^2.0.1",
1134
+ "normalize-path": "^3.0.0",
1135
+ "readable-stream": "^4.0.0"
1136
+ },
1137
+ "engines": {
1138
+ "node": ">= 14"
1139
+ }
1140
+ },
1141
  "node_modules/content-disposition": {
1142
  "version": "0.5.4",
1143
  "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
 
1174
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
1175
  "license": "MIT"
1176
  },
1177
+ "node_modules/core-util-is": {
1178
+ "version": "1.0.3",
1179
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
1180
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
1181
+ "license": "MIT"
1182
+ },
1183
  "node_modules/cors": {
1184
  "version": "2.8.6",
1185
  "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
 
1197
  "url": "https://opencollective.com/express"
1198
  }
1199
  },
1200
+ "node_modules/crc-32": {
1201
+ "version": "1.2.2",
1202
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
1203
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
1204
+ "license": "Apache-2.0",
1205
+ "bin": {
1206
+ "crc32": "bin/crc32.njs"
1207
+ },
1208
+ "engines": {
1209
+ "node": ">=0.8"
1210
+ }
1211
+ },
1212
+ "node_modules/crc32-stream": {
1213
+ "version": "6.0.0",
1214
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
1215
+ "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
1216
+ "license": "MIT",
1217
+ "dependencies": {
1218
+ "crc-32": "^1.2.0",
1219
+ "readable-stream": "^4.0.0"
1220
+ },
1221
+ "engines": {
1222
+ "node": ">= 14"
1223
+ }
1224
+ },
1225
+ "node_modules/cross-spawn": {
1226
+ "version": "7.0.6",
1227
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1228
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1229
+ "license": "MIT",
1230
+ "dependencies": {
1231
+ "path-key": "^3.1.0",
1232
+ "shebang-command": "^2.0.0",
1233
+ "which": "^2.0.1"
1234
+ },
1235
+ "engines": {
1236
+ "node": ">= 8"
1237
+ }
1238
+ },
1239
  "node_modules/debug": {
1240
  "version": "2.6.9",
1241
  "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
 
1354
  "node": ">= 0.4"
1355
  }
1356
  },
1357
+ "node_modules/eastasianwidth": {
1358
+ "version": "0.2.0",
1359
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
1360
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
1361
+ "license": "MIT"
1362
+ },
1363
  "node_modules/ecdsa-sig-formatter": {
1364
  "version": "1.0.11",
1365
  "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
 
1379
  "version": "8.0.0",
1380
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1381
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1382
+ "license": "MIT"
 
1383
  },
1384
  "node_modules/encodeurl": {
1385
  "version": "2.0.0",
 
1450
  "node": ">= 0.6"
1451
  }
1452
  },
1453
+ "node_modules/event-target-shim": {
1454
+ "version": "5.0.1",
1455
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
1456
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
1457
+ "license": "MIT",
1458
+ "engines": {
1459
+ "node": ">=6"
1460
+ }
1461
+ },
1462
+ "node_modules/events": {
1463
+ "version": "3.3.0",
1464
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
1465
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
1466
+ "license": "MIT",
1467
+ "engines": {
1468
+ "node": ">=0.8.x"
1469
+ }
1470
+ },
1471
+ "node_modules/events-universal": {
1472
+ "version": "1.0.1",
1473
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
1474
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
1475
+ "license": "Apache-2.0",
1476
+ "dependencies": {
1477
+ "bare-events": "^2.7.0"
1478
+ }
1479
+ },
1480
  "node_modules/express": {
1481
  "version": "4.22.1",
1482
  "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
 
1535
  "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1536
  "license": "MIT"
1537
  },
1538
+ "node_modules/fast-fifo": {
1539
+ "version": "1.3.2",
1540
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
1541
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
1542
+ "license": "MIT"
1543
+ },
1544
  "node_modules/finalhandler": {
1545
  "version": "1.3.2",
1546
  "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
 
1596
  "unicode-trie": "^2.0.0"
1597
  }
1598
  },
1599
+ "node_modules/foreground-child": {
1600
+ "version": "3.3.1",
1601
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
1602
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
1603
+ "license": "ISC",
1604
+ "dependencies": {
1605
+ "cross-spawn": "^7.0.6",
1606
+ "signal-exit": "^4.0.1"
1607
+ },
1608
+ "engines": {
1609
+ "node": ">=14"
1610
+ },
1611
+ "funding": {
1612
+ "url": "https://github.com/sponsors/isaacs"
1613
+ }
1614
+ },
1615
  "node_modules/form-data": {
1616
  "version": "4.0.5",
1617
  "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
 
1735
  "node": ">= 0.4"
1736
  }
1737
  },
1738
+ "node_modules/glob": {
1739
+ "version": "10.5.0",
1740
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
1741
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
1742
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
1743
+ "license": "ISC",
1744
+ "dependencies": {
1745
+ "foreground-child": "^3.1.0",
1746
+ "jackspeak": "^3.1.2",
1747
+ "minimatch": "^9.0.4",
1748
+ "minipass": "^7.1.2",
1749
+ "package-json-from-dist": "^1.0.0",
1750
+ "path-scurry": "^1.11.1"
1751
+ },
1752
+ "bin": {
1753
+ "glob": "dist/esm/bin.mjs"
1754
+ },
1755
+ "funding": {
1756
+ "url": "https://github.com/sponsors/isaacs"
1757
+ }
1758
+ },
1759
  "node_modules/google-auth-library": {
1760
  "version": "9.15.1",
1761
  "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
 
1837
  "url": "https://github.com/sponsors/ljharb"
1838
  }
1839
  },
1840
+ "node_modules/graceful-fs": {
1841
+ "version": "4.2.11",
1842
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1843
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1844
+ "license": "ISC"
1845
+ },
1846
  "node_modules/gtoken": {
1847
  "version": "7.1.0",
1848
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
 
1963
  "node": ">=0.10.0"
1964
  }
1965
  },
1966
+ "node_modules/ieee754": {
1967
+ "version": "1.2.1",
1968
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
1969
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
1970
+ "funding": [
1971
+ {
1972
+ "type": "github",
1973
+ "url": "https://github.com/sponsors/feross"
1974
+ },
1975
+ {
1976
+ "type": "patreon",
1977
+ "url": "https://www.patreon.com/feross"
1978
+ },
1979
+ {
1980
+ "type": "consulting",
1981
+ "url": "https://feross.org/support"
1982
+ }
1983
+ ],
1984
+ "license": "BSD-3-Clause"
1985
+ },
1986
  "node_modules/inherits": {
1987
  "version": "2.0.4",
1988
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
 
2018
  "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
2019
  "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
2020
  "license": "MIT",
 
2021
  "engines": {
2022
  "node": ">=8"
2023
  }
 
2067
  "url": "https://github.com/sponsors/sindresorhus"
2068
  }
2069
  },
2070
+ "node_modules/isarray": {
2071
+ "version": "1.0.0",
2072
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
2073
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
2074
+ "license": "MIT"
2075
+ },
2076
+ "node_modules/isexe": {
2077
+ "version": "2.0.0",
2078
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
2079
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
2080
+ "license": "ISC"
2081
+ },
2082
+ "node_modules/jackspeak": {
2083
+ "version": "3.4.3",
2084
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
2085
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
2086
+ "license": "BlueOak-1.0.0",
2087
+ "dependencies": {
2088
+ "@isaacs/cliui": "^8.0.2"
2089
+ },
2090
+ "funding": {
2091
+ "url": "https://github.com/sponsors/isaacs"
2092
+ },
2093
+ "optionalDependencies": {
2094
+ "@pkgjs/parseargs": "^0.11.0"
2095
+ }
2096
+ },
2097
  "node_modules/js-md5": {
2098
  "version": "0.8.3",
2099
  "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
 
2130
  "safe-buffer": "^5.0.1"
2131
  }
2132
  },
2133
+ "node_modules/lazystream": {
2134
+ "version": "1.0.1",
2135
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
2136
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
2137
+ "license": "MIT",
2138
+ "dependencies": {
2139
+ "readable-stream": "^2.0.5"
2140
+ },
2141
+ "engines": {
2142
+ "node": ">= 0.6.3"
2143
+ }
2144
+ },
2145
+ "node_modules/lazystream/node_modules/readable-stream": {
2146
+ "version": "2.3.8",
2147
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
2148
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
2149
+ "license": "MIT",
2150
+ "dependencies": {
2151
+ "core-util-is": "~1.0.0",
2152
+ "inherits": "~2.0.3",
2153
+ "isarray": "~1.0.0",
2154
+ "process-nextick-args": "~2.0.0",
2155
+ "safe-buffer": "~5.1.1",
2156
+ "string_decoder": "~1.1.1",
2157
+ "util-deprecate": "~1.0.1"
2158
+ }
2159
+ },
2160
+ "node_modules/lazystream/node_modules/safe-buffer": {
2161
+ "version": "5.1.2",
2162
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
2163
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
2164
+ "license": "MIT"
2165
+ },
2166
+ "node_modules/lazystream/node_modules/string_decoder": {
2167
+ "version": "1.1.1",
2168
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
2169
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
2170
+ "license": "MIT",
2171
+ "dependencies": {
2172
+ "safe-buffer": "~5.1.0"
2173
+ }
2174
+ },
2175
  "node_modules/linebreak": {
2176
  "version": "1.1.0",
2177
  "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
 
2191
  "node": ">= 0.4"
2192
  }
2193
  },
2194
+ "node_modules/lodash": {
2195
+ "version": "4.17.23",
2196
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
2197
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
2198
+ "license": "MIT"
2199
+ },
2200
+ "node_modules/lru-cache": {
2201
+ "version": "10.4.3",
2202
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
2203
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
2204
+ "license": "ISC"
2205
+ },
2206
  "node_modules/math-intrinsics": {
2207
  "version": "1.1.0",
2208
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
 
2272
  "node": ">= 0.6"
2273
  }
2274
  },
2275
+ "node_modules/minimatch": {
2276
+ "version": "9.0.9",
2277
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
2278
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
2279
+ "license": "ISC",
2280
+ "dependencies": {
2281
+ "brace-expansion": "^2.0.2"
2282
+ },
2283
+ "engines": {
2284
+ "node": ">=16 || 14 >=14.17"
2285
+ },
2286
+ "funding": {
2287
+ "url": "https://github.com/sponsors/isaacs"
2288
+ }
2289
+ },
2290
+ "node_modules/minipass": {
2291
+ "version": "7.1.3",
2292
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
2293
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
2294
+ "license": "BlueOak-1.0.0",
2295
+ "engines": {
2296
+ "node": ">=16 || 14 >=14.17"
2297
+ }
2298
+ },
2299
  "node_modules/ms": {
2300
  "version": "2.0.0",
2301
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
 
2343
  }
2344
  }
2345
  },
2346
+ "node_modules/normalize-path": {
2347
+ "version": "3.0.0",
2348
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
2349
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
2350
+ "license": "MIT",
2351
+ "engines": {
2352
+ "node": ">=0.10.0"
2353
+ }
2354
+ },
2355
  "node_modules/object-assign": {
2356
  "version": "4.1.1",
2357
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 
2403
  "url": "https://github.com/sponsors/sindresorhus"
2404
  }
2405
  },
2406
+ "node_modules/package-json-from-dist": {
2407
+ "version": "1.0.1",
2408
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
2409
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
2410
+ "license": "BlueOak-1.0.0"
2411
+ },
2412
  "node_modules/pako": {
2413
  "version": "0.2.9",
2414
  "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
 
2424
  "node": ">= 0.8"
2425
  }
2426
  },
2427
+ "node_modules/path-key": {
2428
+ "version": "3.1.1",
2429
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2430
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2431
+ "license": "MIT",
2432
+ "engines": {
2433
+ "node": ">=8"
2434
+ }
2435
+ },
2436
+ "node_modules/path-scurry": {
2437
+ "version": "1.11.1",
2438
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
2439
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
2440
+ "license": "BlueOak-1.0.0",
2441
+ "dependencies": {
2442
+ "lru-cache": "^10.2.0",
2443
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
2444
+ },
2445
+ "engines": {
2446
+ "node": ">=16 || 14 >=14.18"
2447
+ },
2448
+ "funding": {
2449
+ "url": "https://github.com/sponsors/isaacs"
2450
+ }
2451
+ },
2452
  "node_modules/path-to-regexp": {
2453
  "version": "0.1.12",
2454
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
 
2474
  "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
2475
  "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
2476
  },
2477
+ "node_modules/process": {
2478
+ "version": "0.11.10",
2479
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
2480
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
2481
+ "license": "MIT",
2482
+ "engines": {
2483
+ "node": ">= 0.6.0"
2484
+ }
2485
+ },
2486
+ "node_modules/process-nextick-args": {
2487
+ "version": "2.0.1",
2488
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
2489
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
2490
+ "license": "MIT"
2491
+ },
2492
  "node_modules/proxy-addr": {
2493
  "version": "2.0.7",
2494
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 
2547
  "node": ">= 0.8"
2548
  }
2549
  },
2550
+ "node_modules/readable-stream": {
2551
+ "version": "4.7.0",
2552
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
2553
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
2554
+ "license": "MIT",
2555
+ "dependencies": {
2556
+ "abort-controller": "^3.0.0",
2557
+ "buffer": "^6.0.3",
2558
+ "events": "^3.3.0",
2559
+ "process": "^0.11.10",
2560
+ "string_decoder": "^1.3.0"
2561
+ },
2562
+ "engines": {
2563
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
2564
+ }
2565
+ },
2566
+ "node_modules/readdir-glob": {
2567
+ "version": "1.1.3",
2568
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
2569
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
2570
+ "license": "Apache-2.0",
2571
+ "dependencies": {
2572
+ "minimatch": "^5.1.0"
2573
+ }
2574
+ },
2575
+ "node_modules/readdir-glob/node_modules/minimatch": {
2576
+ "version": "5.1.9",
2577
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
2578
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
2579
+ "license": "ISC",
2580
+ "dependencies": {
2581
+ "brace-expansion": "^2.0.1"
2582
+ },
2583
+ "engines": {
2584
+ "node": ">=10"
2585
+ }
2586
+ },
2587
  "node_modules/restructure": {
2588
  "version": "3.0.2",
2589
  "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
 
2735
  "@img/sharp-win32-x64": "0.34.5"
2736
  }
2737
  },
2738
+ "node_modules/shebang-command": {
2739
+ "version": "2.0.0",
2740
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2741
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2742
+ "license": "MIT",
2743
+ "dependencies": {
2744
+ "shebang-regex": "^3.0.0"
2745
+ },
2746
+ "engines": {
2747
+ "node": ">=8"
2748
+ }
2749
+ },
2750
+ "node_modules/shebang-regex": {
2751
+ "version": "3.0.0",
2752
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2753
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2754
+ "license": "MIT",
2755
+ "engines": {
2756
+ "node": ">=8"
2757
+ }
2758
+ },
2759
  "node_modules/side-channel": {
2760
  "version": "1.1.0",
2761
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
 
2828
  "url": "https://github.com/sponsors/ljharb"
2829
  }
2830
  },
2831
+ "node_modules/signal-exit": {
2832
+ "version": "4.1.0",
2833
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
2834
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
2835
+ "license": "ISC",
2836
+ "engines": {
2837
+ "node": ">=14"
2838
+ },
2839
+ "funding": {
2840
+ "url": "https://github.com/sponsors/isaacs"
2841
+ }
2842
+ },
2843
  "node_modules/statuses": {
2844
  "version": "2.0.2",
2845
  "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
 
2849
  "node": ">= 0.8"
2850
  }
2851
  },
2852
+ "node_modules/streamx": {
2853
+ "version": "2.25.0",
2854
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
2855
+ "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
2856
+ "license": "MIT",
2857
+ "dependencies": {
2858
+ "events-universal": "^1.0.0",
2859
+ "fast-fifo": "^1.3.2",
2860
+ "text-decoder": "^1.1.0"
2861
+ }
2862
+ },
2863
+ "node_modules/string_decoder": {
2864
+ "version": "1.3.0",
2865
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
2866
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
2867
+ "license": "MIT",
2868
+ "dependencies": {
2869
+ "safe-buffer": "~5.2.0"
2870
+ }
2871
+ },
2872
  "node_modules/string-width": {
2873
  "version": "4.2.3",
2874
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2875
  "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2876
  "license": "MIT",
2877
+ "dependencies": {
2878
+ "emoji-regex": "^8.0.0",
2879
+ "is-fullwidth-code-point": "^3.0.0",
2880
+ "strip-ansi": "^6.0.1"
2881
+ },
2882
+ "engines": {
2883
+ "node": ">=8"
2884
+ }
2885
+ },
2886
+ "node_modules/string-width-cjs": {
2887
+ "name": "string-width",
2888
+ "version": "4.2.3",
2889
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2890
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2891
+ "license": "MIT",
2892
  "dependencies": {
2893
  "emoji-regex": "^8.0.0",
2894
  "is-fullwidth-code-point": "^3.0.0",
 
2903
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2904
  "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2905
  "license": "MIT",
 
2906
  "dependencies": {
2907
  "ansi-regex": "^5.0.1"
2908
  },
 
2910
  "node": ">=8"
2911
  }
2912
  },
2913
+ "node_modules/strip-ansi-cjs": {
2914
+ "name": "strip-ansi",
2915
+ "version": "6.0.1",
2916
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2917
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2918
+ "license": "MIT",
2919
+ "dependencies": {
2920
+ "ansi-regex": "^5.0.1"
2921
+ },
2922
+ "engines": {
2923
+ "node": ">=8"
2924
+ }
2925
+ },
2926
+ "node_modules/tar-stream": {
2927
+ "version": "3.1.8",
2928
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
2929
+ "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
2930
+ "license": "MIT",
2931
+ "dependencies": {
2932
+ "b4a": "^1.6.4",
2933
+ "bare-fs": "^4.5.5",
2934
+ "fast-fifo": "^1.2.0",
2935
+ "streamx": "^2.15.0"
2936
+ }
2937
+ },
2938
+ "node_modules/teex": {
2939
+ "version": "1.0.1",
2940
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
2941
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
2942
+ "license": "MIT",
2943
+ "dependencies": {
2944
+ "streamx": "^2.12.5"
2945
+ }
2946
+ },
2947
+ "node_modules/text-decoder": {
2948
+ "version": "1.2.7",
2949
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
2950
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
2951
+ "license": "Apache-2.0",
2952
+ "dependencies": {
2953
+ "b4a": "^1.6.4"
2954
+ }
2955
+ },
2956
  "node_modules/tiny-inflate": {
2957
  "version": "1.0.3",
2958
  "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
 
3028
  "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
3029
  "license": "BSD"
3030
  },
3031
+ "node_modules/util-deprecate": {
3032
+ "version": "1.0.2",
3033
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
3034
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
3035
+ "license": "MIT"
3036
+ },
3037
  "node_modules/utils-merge": {
3038
  "version": "1.0.1",
3039
  "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
 
3077
  "webidl-conversions": "^3.0.0"
3078
  }
3079
  },
3080
+ "node_modules/which": {
3081
+ "version": "2.0.2",
3082
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
3083
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
3084
+ "license": "ISC",
3085
+ "dependencies": {
3086
+ "isexe": "^2.0.0"
3087
+ },
3088
+ "bin": {
3089
+ "node-which": "bin/node-which"
3090
+ },
3091
+ "engines": {
3092
+ "node": ">= 8"
3093
+ }
3094
+ },
3095
+ "node_modules/wrap-ansi": {
3096
+ "version": "8.1.0",
3097
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
3098
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
3099
+ "license": "MIT",
3100
+ "dependencies": {
3101
+ "ansi-styles": "^6.1.0",
3102
+ "string-width": "^5.0.1",
3103
+ "strip-ansi": "^7.0.1"
3104
+ },
3105
+ "engines": {
3106
+ "node": ">=12"
3107
+ },
3108
+ "funding": {
3109
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
3110
+ }
3111
+ },
3112
+ "node_modules/wrap-ansi-cjs": {
3113
+ "name": "wrap-ansi",
3114
+ "version": "7.0.0",
3115
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
3116
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
3117
+ "license": "MIT",
3118
+ "dependencies": {
3119
+ "ansi-styles": "^4.0.0",
3120
+ "string-width": "^4.1.0",
3121
+ "strip-ansi": "^6.0.0"
3122
+ },
3123
+ "engines": {
3124
+ "node": ">=10"
3125
+ },
3126
+ "funding": {
3127
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
3128
+ }
3129
+ },
3130
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
3131
+ "version": "4.3.0",
3132
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
3133
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
3134
+ "license": "MIT",
3135
+ "dependencies": {
3136
+ "color-convert": "^2.0.1"
3137
+ },
3138
+ "engines": {
3139
+ "node": ">=8"
3140
+ },
3141
+ "funding": {
3142
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
3143
+ }
3144
+ },
3145
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
3146
+ "version": "6.2.2",
3147
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
3148
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
3149
+ "license": "MIT",
3150
+ "engines": {
3151
+ "node": ">=12"
3152
+ },
3153
+ "funding": {
3154
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
3155
+ }
3156
+ },
3157
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
3158
+ "version": "9.2.2",
3159
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
3160
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
3161
+ "license": "MIT"
3162
+ },
3163
+ "node_modules/wrap-ansi/node_modules/string-width": {
3164
+ "version": "5.1.2",
3165
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
3166
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
3167
+ "license": "MIT",
3168
+ "dependencies": {
3169
+ "eastasianwidth": "^0.2.0",
3170
+ "emoji-regex": "^9.2.2",
3171
+ "strip-ansi": "^7.0.1"
3172
+ },
3173
+ "engines": {
3174
+ "node": ">=12"
3175
+ },
3176
+ "funding": {
3177
+ "url": "https://github.com/sponsors/sindresorhus"
3178
+ }
3179
+ },
3180
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
3181
+ "version": "7.2.0",
3182
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
3183
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
3184
+ "license": "MIT",
3185
+ "dependencies": {
3186
+ "ansi-regex": "^6.2.2"
3187
+ },
3188
+ "engines": {
3189
+ "node": ">=12"
3190
+ },
3191
+ "funding": {
3192
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
3193
+ }
3194
+ },
3195
  "node_modules/wsl-utils": {
3196
  "version": "0.1.0",
3197
  "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
 
3206
  "funding": {
3207
  "url": "https://github.com/sponsors/sindresorhus"
3208
  }
3209
+ },
3210
+ "node_modules/zip-stream": {
3211
+ "version": "6.0.1",
3212
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
3213
+ "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
3214
+ "license": "MIT",
3215
+ "dependencies": {
3216
+ "archiver-utils": "^5.0.0",
3217
+ "compress-commons": "^6.0.2",
3218
+ "readable-stream": "^4.0.0"
3219
+ },
3220
+ "engines": {
3221
+ "node": ">= 14"
3222
+ }
3223
  }
3224
  }
3225
  }
package.json CHANGED
@@ -11,6 +11,7 @@
11
  "@huggingface/hub": "^2.11.0",
12
  "@huggingface/inference": "^4.13.15",
13
  "@zernio/node": "^0.2.4",
 
14
  "axios": "^1.6.2",
15
  "cors": "^2.8.5",
16
  "dotenv": "^16.3.1",
 
11
  "@huggingface/hub": "^2.11.0",
12
  "@huggingface/inference": "^4.13.15",
13
  "@zernio/node": "^0.2.4",
14
+ "archiver": "^7.0.1",
15
  "axios": "^1.6.2",
16
  "cors": "^2.8.5",
17
  "dotenv": "^16.3.1",
public/social-dashboard.html CHANGED
@@ -2,498 +2,641 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>VinOS — Social Media Autopilot</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <link rel="stylesheet" href="style.css">
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
  <style>
10
- .dashboard-grid {
11
- display: grid;
12
- grid-template-columns: 2fr 1fr;
13
- gap: 24px;
14
- padding: 24px;
15
- max-width: 1400px;
16
- margin: auto;
17
- }
18
- .social-card {
19
- margin-bottom: 24px;
20
- }
21
- .platform-badge {
22
- padding: 4px 12px;
23
- border-radius: 20px;
24
- font-size: 0.8rem;
25
- font-weight: bold;
26
- text-transform: uppercase;
27
- }
28
- .badge-ig { background: #E1306C; color: white; }
29
- .badge-threads { background: #000; color: white; border: 1px solid #333; }
30
-
31
- .toggle-container {
32
- display: flex;
33
- align-items: center;
34
- gap: 12px;
35
- }
36
- .switch {
37
- position: relative;
38
- display: inline-block;
39
- width: 50px;
40
- height: 24px;
41
- }
42
- .switch input { opacity: 0; width: 0; height: 0; }
43
- .slider {
44
- position: absolute;
45
- cursor: pointer;
46
- top: 0; left: 0; right: 0; bottom: 0;
47
- background-color: #333;
48
- transition: .4s;
49
- border-radius: 34px;
50
- }
51
- .slider:before {
52
- position: absolute;
53
- content: "";
54
- height: 16px; width: 16px;
55
- left: 4px; bottom: 4px;
56
- background-color: white;
57
- transition: .4s;
58
- border-radius: 50%;
59
- }
60
- input:checked + .slider { background-color: var(--accent-purple); }
61
- input:checked + .slider:before { transform: translateX(26px); }
62
 
63
- .research-input {
64
- display: flex;
65
- gap: 12px;
66
- margin-top: 12px;
67
- }
68
- .research-input input {
69
- flex: 1;
70
- background: rgba(255,255,255,0.05);
71
- border: 1px solid var(--glass-border);
72
- border-radius: 12px;
73
- padding: 12px;
74
- color: white;
75
- }
76
 
77
- .post-draft-grid {
78
- display: grid;
79
- grid-template-columns: 1fr 1fr;
80
- gap: 20px;
81
- }
82
- .variant-box {
83
- background: rgba(255,255,255,0.02);
84
- border: 1px dashed rgba(255,255,255,0.1);
85
- border-radius: 12px;
86
- padding: 15px;
87
- font-size: 0.9rem;
88
- margin-top: 10px;
89
- }
90
- .image-preview {
91
- width: 100%;
92
- height: 200px;
93
- background-size: cover;
94
- background-position: center;
95
- border-radius: 12px;
96
- margin-top: 10px;
97
- background-color: #1a1b23;
98
- border: 1px solid rgba(255,255,255,0.1);
99
- }
100
 
101
- .status-pill {
102
- display: flex;
103
- align-items: center;
104
- gap: 8px;
105
- background: rgba(255,255,255,0.05);
106
- padding: 8px 16px;
107
- border-radius: 12px;
108
- font-size: 0.9rem;
109
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  </style>
111
  </head>
112
  <body class="scroll-thin">
113
 
114
- <div style="padding: 24px 24px 0 24px; max-width: 1400px; margin: auto;">
115
- <div style="display: flex; justify-content: space-between; align-items: center;">
116
- <div>
117
- <h1 style="margin:0; font-size: 2.5rem;">Social <span style="font-weight: 300; opacity: 0.7;">Autopilot</span></h1>
118
- <p style="opacity:0.6;">Research → AI Remix → Auto Post (IG & Threads)</p>
119
- </div>
120
- <div class="toggle-container">
121
- <span id="autopilot-label" style="font-weight: 600;">Autopilot: OFF</span>
122
- <label class="switch">
123
- <input type="checkbox" id="autopilot-toggle">
124
- <span class="slider"></span>
125
- </label>
126
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </div>
128
 
129
- <div style="display: flex; gap: 15px; margin-top: 20px;" id="account-status">
130
- <!-- Dynamic account pills -->
131
- <div class="status-pill"><div class="pulse-dot" style="background:gray; box-shadow:none;"></div> Checking Zernio...</div>
 
 
 
 
 
 
 
 
132
  </div>
133
- </div>
134
 
135
- <div class="dashboard-grid">
136
- <!-- Main Column -->
137
- <div class="left-col">
138
- <h2 style="margin-bottom: 20px;">🧠 Brainstorming</h2>
139
- <div class="glass-card social-card" style="margin-bottom: 25px;">
140
- <p style="font-size:0.8rem; opacity:0.6; margin-top:-10px;">No source URL? Enter a topic to generate ideas from scratch.</p>
141
- <div style="display: flex; gap: 10px; margin-top: 15px;">
142
- <input type="text" id="brainstorm-topic" placeholder="e.g. Tips for AI automation..."
143
- style="flex: 1; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; padding: 10px;">
144
- <button class="btn-primary" id="btn-brainstorm" style="background: linear-gradient(135deg, #10b981, #3b82f6); white-space: nowrap;">Brainstorm</button>
145
- </div>
146
- <div id="brainstorm-info" style="margin-top:10px; font-size: 0.8rem; color: #10b981;"></div>
147
  </div>
 
148
 
149
- <h2 style="margin-bottom: 20px;">🔍 Research & Remix</h2>
150
- <div class="glass-card social-card">
151
- <h3>🚀 Source Post Research</h3>
152
- <p style="font-size:0.9rem; margin-top:-10px; opacity:0.7;">Paste any post URL (X, Threads, IG) to trigger AI remixing.</p>
153
- <div class="research-input">
154
- <input type="text" id="research-url" placeholder="https://threads.net/@user/post/...">
155
- <button class="btn-primary" id="btn-research">Analyze URL</button>
156
- <button class="btn-primary" style="background: var(--accent-pink);" id="btn-sync-sheets">Init Sheet</button>
157
- </div>
158
- <div id="research-info" style="margin-top:10px; font-size: 0.8rem; color: var(--accent-blue);"></div>
159
  </div>
 
 
 
 
 
 
 
 
160
 
161
- <h2 style="margin-top: 40px; margin-bottom: 20px;">📅 Post Drafts</h2>
162
- <div id="posts-container">
163
- <!-- Posts dynamic -->
164
- <div class="glass-card" style="text-align: center; opacity: 0.5;">
165
- <p>No drafts yet. Pasteurize a URL above to start.</p>
166
- </div>
 
 
 
 
 
 
167
  </div>
168
  </div>
169
 
170
- <!-- Right Column (Analytics & Accounts) -->
171
- <div class="right-col">
172
- <div class="glass-card social-card">
173
- <h3>📈 Growth Tracker</h3>
174
- <canvas id="growthChart" height="200"></canvas>
 
 
175
  </div>
176
-
177
- <div class="glass-card social-card">
178
- <h3>🔗 Connect Accounts</h3>
179
- <div style="display: flex; flex-direction: column; gap: 10px;">
180
- <button class="btn-primary" onclick="connectAccount('instagram')" style="background: #E1306C;">Instagram (Priority)</button>
181
- <button class="btn-primary" onclick="connectAccount('threads')" style="background: #000; border: 1px solid #333;">Connect Threads</button>
182
- <button class="btn-primary" onclick="connectAccount('facebook')" style="background: #1877F2;">Connect Facebook</button>
183
- <button class="btn-primary" onclick="connectAccount('pinterest')" style="background: #BD081C;">Connect Pinterest</button>
184
- <button class="btn-primary" onclick="connectAccount('twitter')" style="background: #1DA1F2;">X (PostProxy)</button>
185
- <button class="btn-primary" onclick="connectAccount('linkedin')" style="background: #0077b5;">LinkedIn (PostProxy)</button>
186
- </div>
187
  </div>
 
188
 
189
- <div class="glass-card social-card" id="quota-card">
190
- <h3>📊 Monthly Quota</h3>
191
- <div style="margin-top: 15px;">
192
- <div style="display: flex; justify-content: space-between; font-size: 0.9rem; margin-bottom: 5px;">
193
- <span>Usage (Post/Month)</span>
194
- <span id="quota-text">0 / 20</span>
195
- </div>
196
- <div style="width: 100%; height: 8px; background: rgba(255,255,255,0.05); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
197
- <div id="quota-bar" style="width: 0%; height: 100%; background: linear-gradient(to right, var(--accent-blue), var(--accent-pink)); transition: width 0.5s ease;"></div>
198
- </div>
199
- <p style="font-size: 0.7rem; opacity: 0.5; margin-top: 10px;">Resets on the 1st of every month.</p>
200
- </div>
201
- </div>
202
 
203
- <div class="glass-card social-card">
204
- <h3>📈 Performance</h3>
205
- <div style="display: flex; flex-direction: column; gap: 15px; margin-top: 15px;">
206
- <div style="display: flex; justify-content: space-between;">
207
- <span>Avg Engagement</span>
208
- <span class="stat-value" style="font-size: 1.2rem;">4.2%</span>
209
- </div>
210
- <div style="display: flex; justify-content: space-between;">
211
- <span>Total Published</span>
212
- <span class="stat-value" style="font-size: 1.2rem;" id="stat-total-posts">0</span>
213
- </div>
214
- </div>
215
  </div>
216
  </div>
217
  </div>
218
-
219
- <script>
220
- async function loadStatus() {
221
- // 1. Zernio (IG/Threads)
222
- const res = await fetch('/api/social/status');
223
- const data = await res.json();
224
-
225
- // 2. PostProxy (LinkedIn/X)
226
- const ppRes = await fetch('/api/social/postproxy-profiles');
227
- const ppData = await ppRes.json();
228
-
229
- if (data.success) {
230
- const zAccs = (data.accounts || []).map(a => `
231
- <div class="status-pill">
232
- <div class="pulse-dot"></div>
233
- ${a.platform.toUpperCase()} (${a.username})
234
- </div>
235
- `);
236
-
237
- const ppAccs = (ppData.profiles || []).map(a => `
238
- <div class="status-pill">
239
- <div class="pulse-dot" style="background: #3b82f6;"></div>
240
- ${a.platform.toUpperCase()} (${a.name || a.id})
241
- </div>
242
- `);
243
-
244
- document.getElementById('account-status').innerHTML = [...zAccs, ...ppAccs].join('') || '<div class="status-pill" style="opacity:0.5;">No accounts connected</div>';
245
-
246
- const toggle = document.getElementById('autopilot-toggle');
247
- toggle.checked = data.autopilot;
248
- document.getElementById('autopilot-label').innerText = `Autopilot: ${data.autopilot ? 'ON' : 'OFF'}`;
249
- document.getElementById('stat-total-posts').innerText = data.postCount;
250
-
251
- // Update Quota UI
252
- if (data.quota) {
253
- const q = data.quota;
254
- const percent = (q.posts_this_month / q.limit) * 100;
255
- document.getElementById('quota-text').innerText = `${q.posts_this_month} / ${q.limit}`;
256
- document.getElementById('quota-bar').style.width = `${percent}%`;
257
- }
258
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  }
260
-
261
- async function loadPosts() {
262
- const res = await fetch('/api/social/posts');
263
- const posts = await res.json();
264
- const container = document.getElementById('posts-container');
265
- if (posts.length === 0) return;
266
-
267
- container.innerHTML = posts.map(post => {
268
- const v1 = post.v1 || {};
269
- const v2 = post.v2 || { slides: [] };
270
- const isApproved = post.status !== 'draft';
271
- const platforms = ['IG', 'Threads', 'LinkedIn', 'X', 'FB', 'Pinterest'];
272
- const badges = platforms.map(p => `<span class="platform-badge badge-${p.toLowerCase()}">${p}</span>`).join(' ');
273
- const hasCarousel = v2.slides && v2.slides.length > 0;
274
-
275
- // Image provider badge
276
- const srcMap = {
277
- 'nano-banana': { icon: '🍌', label: 'Nano Banana', color: '#f59e0b' },
278
- 'hive-flux': { icon: '⚡', label: 'Hive AI Flux', color: '#8b5cf6' },
279
- 'hf-flux': { icon: '🤗', label: 'HF FLUX', color: '#3b82f6' },
280
- 'nvidia-sdxl': { icon: '🖥️', label: 'NVIDIA SDXL', color: '#10b981' }
281
- };
282
- const srcInfo = srcMap[post.imageSource] || { icon: '🤖', label: post.imageSource || 'AI', color: '#6b7280' };
283
- const costLabel = post.costIDR
284
- ? `<span style="color:#f59e0b; font-size:10px;"> · Rp ${parseInt(post.costIDR).toLocaleString('id-ID')}</span>`
285
- : '';
286
-
287
- return `
288
- <div class="glass-card social-card" style="position: relative;" id="card-${post.id}">
289
- <div style="position: absolute; top: 15px; right: 20px;">
290
- ${badges}
291
- </div>
292
- <h4 style="margin:0; opacity: 0.6; font-size: 0.8rem;">ID: ${post.id} ${hasCarousel ? '🎡 (Carousel)' : ''}</h4>
293
-
294
- <div class="post-draft-grid">
295
- <div id="preview-${post.id}">
296
- ${post.type === 'carousel' && post.mediaUrls ? `
297
- <div style="display: flex; gap: 10px; overflow-x: auto; padding-bottom: 15px; white-space: nowrap;">
298
- ${post.mediaUrls.map(url => `<div class="image-preview" style="display: inline-block; flex: 0 0 auto; width: 140px; height: 175px; background-image: url('${url}'); background-size: cover; border-radius: 8px;"></div>`).join('')}
299
- </div>
300
- <p style="font-size: 11px; color: var(--accent-blue); margin-top:-5px; margin-bottom:10px;">${post.mediaUrls.length} Slides Carousel (Scroll ➡)</p>
301
- ` : `
302
- <div class="image-preview" style="background-image: url('${v1.mediaUrl}');"></div>
303
- `}
304
- <div style="margin-top: 10px; font-weight: bold;">Draft Caption:</div>
305
- <textarea id="text-${post.id}" class="variant-box" style="width: 100%; height: 120px; background: rgba(0,0,0,0.3); color: white; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; font-size: 13px;" ${isApproved ? 'readonly' : ''}>${v1.ig_id || v1.ig_en}</textarea>
306
- ${hasCarousel && post.type !== 'carousel' ? `<p style="font-size: 10px; color: #3b82f6; margin-top:5px;">+ ${v2.slides.length} Text Carousel Slides</p>` : ''}
307
- </div>
308
- <div>
309
- <div style="font-weight: bold; margin-bottom: 5px;">Status: <span style="color: ${isApproved ? '#10b981' : 'var(--accent-pink)'};">${post.status.toUpperCase()}</span></div>
310
- <div style="font-size: 11px; opacity: 0.7; margin-bottom: 8px;">
311
- <span style="color: ${srcInfo.color};">${srcInfo.icon} ${srcInfo.label}</span>${costLabel}
312
- </div>
313
-
314
- ${v1.pillar ? `
315
- <div style="margin-bottom: 8px;">
316
- <span style="background: rgba(99,102,241,0.2); color: #a5b4fc; border: 1px solid rgba(99,102,241,0.3); border-radius: 20px; padding: 2px 10px; font-size: 10px; text-transform: uppercase; letter-spacing: 1px;">
317
- 📌 ${v1.pillar}
318
- </span>
319
- </div>` : ''}
320
-
321
- ${v1.best_time_to_post ? `
322
- <p style="font-size: 10px; opacity: 0.6; margin: 4px 0;">
323
- ⏰ Best time: ${v1.best_time_to_post}
324
- </p>` : ''}
325
-
326
- ${v1.hashtags && v1.hashtags.length > 0 ? `
327
- <p style="font-size: 10px; color: #3b82f6; margin: 4px 0; word-break: break-word;">
328
- ${v1.hashtags.slice(0, 5).join(' ')}
329
- </p>` : ''}
330
-
331
- ${v1.engagement_notes ? `
332
- <p style="font-size: 10px; opacity: 0.5; font-style: italic; margin: 6px 0 0; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 6px;">
333
- 💡 ${v1.engagement_notes}
334
- </p>` : ''}
335
-
336
- <div style="display: flex; flex-direction: column; gap: 8px; margin-top: 20px;">
337
- <button class="btn-primary" onclick="confirmPost('${post.id}')" ${isApproved ? 'disabled' : ''}>
338
- ${isApproved ? '✅ CONFIRMED' : 'CONFIRM & POST'}
339
- </button>
340
- <button class="btn-primary" onclick="schedulePost('${post.id}')" style="background: var(--accent-purple);" ${isApproved ? 'disabled' : ''}>
341
- REVISE & SCHEDULE
342
- </button>
343
- <button class="btn-primary" style="background: rgba(255,0,0,0.2); border: 1px solid rgba(255,0,0,0.2); color: #ff5555;" onclick="deletePost('${post.id}')" ${isApproved ? 'disabled' : ''}>
344
- DISCARD DRAFT
345
- </button>
346
- </div>
347
- </div>
348
- </div>
349
  </div>
350
- `;
351
- }).join('');
 
 
352
  }
353
 
354
- async function confirmPost(postId) {
355
- const revisedText = document.getElementById(`text-${postId}`).value;
356
- const res = await fetch('/api/social/approve', {
357
- method: 'POST',
358
- headers: { 'Content-Type': 'application/json' },
359
- body: JSON.stringify({ postId, revisedText })
360
- });
361
- const data = await res.json();
362
- if (data.success) {
363
- alert('Success! Content confirmed and published.');
364
- loadPosts();
365
- loadStatus();
366
- } else alert('Error: ' + data.error);
367
- }
368
 
369
- async function schedulePost(postId) {
370
- const revisedText = document.getElementById(`text-${postId}`).value;
371
- const time = prompt('Schedule for (ISO Date):', new Date(Date.now() + 86400000).toISOString().split('.')[0]);
372
- if (time) {
373
- const res = await fetch('/api/social/approve', {
374
- method: 'POST',
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify({ postId, revisedText, scheduleTime: time })
377
- });
378
- const data = await res.json();
379
- if (data.success) {
380
- alert('Scheduled successfully!');
381
- loadPosts();
382
- } else alert('Error: ' + data.error);
383
- }
384
- }
385
 
386
- async function deletePost(postId) {
387
- if (confirm('Discard this draft permanently?')) {
388
- await fetch(`/api/social/delete/${postId}`, { method: 'DELETE' });
389
- loadPosts();
390
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
-
393
- async function connectAccount(platform) {
394
- const res = await fetch(`/api/social/connect/${platform}`);
395
- const data = await res.json();
396
- if (data.success) {
397
- window.open(data.authUrl, '_blank');
398
- } else alert('Error: ' + data.error);
 
 
 
 
399
  }
400
-
401
- document.getElementById('btn-brainstorm').onclick = async () => {
402
- const topic = document.getElementById('brainstorm-topic').value;
403
- if (!topic) return;
404
- document.getElementById('btn-brainstorm').innerText = 'Thinking...';
405
- document.getElementById('brainstorm-info').innerText = 'AI is brainstorming hooks...';
406
-
407
- const res = await fetch('/api/social/brainstorm', {
408
- method: 'POST',
409
- headers: { 'Content-Type': 'application/json' },
410
- body: JSON.stringify({ topic })
411
- });
412
- const data = await res.json();
413
- document.getElementById('btn-brainstorm').innerText = 'Brainstorm';
414
-
415
- if (data.success) {
416
- document.getElementById('brainstorm-info').innerText = 'Ideas generated! Check drafts below.';
417
- loadPosts();
418
- } else {
419
- document.getElementById('brainstorm-info').innerText = 'Error: ' + data.error;
420
- }
421
- };
422
-
423
- document.getElementById('btn-research').onclick = async () => {
424
- const url = document.getElementById('research-url').value;
425
- if (!url) return;
426
- document.getElementById('btn-research').innerText = 'Researching...';
427
- document.getElementById('research-info').innerText = 'Starting Apify Scraper...';
428
-
429
- const res = await fetch('/api/social/research', {
430
- method: 'POST',
431
- headers: { 'Content-Type': 'application/json' },
432
- body: JSON.stringify({ url })
433
- });
434
- const data = await res.json();
435
- document.getElementById('btn-research').innerText = 'Analyze URL';
436
-
437
- if (data.success) {
438
- document.getElementById('research-info').innerText = 'Success! Draft created.';
439
- loadPosts();
440
- } else {
441
- document.getElementById('research-info').innerText = 'Error: ' + data.error;
442
- }
443
- };
444
-
445
- document.getElementById('autopilot-toggle').onchange = async (e) => {
446
- await fetch('/api/social/autopilot', {
447
- method: 'POST',
448
- headers: { 'Content-Type': 'application/json' },
449
- body: JSON.stringify({ enabled: e.target.checked })
450
- });
451
- loadStatus();
452
- };
453
-
454
- document.getElementById('btn-sync-sheets').onclick = async () => {
455
- const res = await fetch('/api/social/init-sheets');
456
- const data = await res.json();
457
- if (data.success) {
458
- if(data.url) {
459
- if(confirm('Google Sheet is ready! Click OK to open it now.\n' + data.url)) {
460
- window.open(data.url, '_blank');
461
- }
462
- } else {
463
- alert('Google Sheet updated with new tabs!');
464
- }
465
- } else {
466
- alert('Failed to initialize sheets: ' + data.error);
467
- }
468
- };
469
-
470
- // Init Chart
471
- const ctx = document.getElementById('growthChart').getContext('2d');
472
- new Chart(ctx, {
473
- type: 'line',
474
  data: {
475
- labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
476
- datasets: [{
477
- label: 'Followers (IG)',
478
- data: [1200, 1250, 1310, 1380, 1450, 1500, 1580],
479
- borderColor: '#E1306C',
480
- tension: 0.4
481
- }, {
482
- label: 'Followers (Threads)',
483
- data: [300, 420, 580, 710, 850, 1020, 1150],
484
- borderColor: '#8b5cf6',
485
- tension: 0.4
486
- }]
487
  },
488
  options: {
489
  plugins: { legend: { display: false } },
490
- scales: { y: { display: false }, x: { grid: { display: false } } }
491
  }
492
  });
493
 
494
- loadStatus();
495
- loadPosts();
496
- setInterval(loadStatus, 30000);
497
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  </body>
499
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>VinOS — Social Autopilot</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <link rel="stylesheet" href="style.css">
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
  <style>
10
+ :root { --sa-green: #10b981; --sa-red: #ef4444; --sa-yellow: #f59e0b; --sa-blue: #3b82f6; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ .sa-layout { display: grid; grid-template-columns: 1fr 340px; gap: 20px; padding: 20px; max-width: 1500px; margin: auto; }
13
+ @media (max-width: 1100px) { .sa-layout { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ /* Header */
16
+ .sa-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 20px 0; max-width: 1500px; margin: auto; flex-wrap: wrap; gap: 15px; }
17
+ .sa-header h1 { margin: 0; font-size: 2.2rem; }
18
+ .sa-header h1 span { font-weight: 300; opacity: 0.6; }
19
+ .sa-accounts { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; padding: 0 20px; max-width: 1500px; margin-left: auto; margin-right: auto; }
20
+ .acct-pill { display: flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); padding: 6px 14px; border-radius: 20px; font-size: 0.82rem; }
21
+ .acct-dot { width: 8px; height: 8px; border-radius: 50%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ /* Toggle */
24
+ .switch { position: relative; display: inline-block; width: 46px; height: 22px; }
25
+ .switch input { opacity: 0; width: 0; height: 0; }
26
+ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #333; transition: .3s; border-radius: 22px; }
27
+ .slider:before { content: ""; position: absolute; height: 14px; width: 14px; left: 4px; bottom: 4px; background: white; transition: .3s; border-radius: 50%; }
28
+ input:checked + .slider { background: var(--sa-green); }
29
+ input:checked + .slider:before { transform: translateX(24px); }
30
+
31
+ /* Pipeline Steps */
32
+ .pipeline-bar { display: flex; align-items: center; gap: 0; margin: 15px 0 20px; overflow-x: auto; padding-bottom: 5px; }
33
+ .pipe-step { display: flex; align-items: center; gap: 0; flex-shrink: 0; }
34
+ .pipe-circle { width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,0.06); border: 2px solid rgba(255,255,255,0.12); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; color: rgba(255,255,255,0.4); transition: all 0.3s; }
35
+ .pipe-circle.active { background: var(--sa-blue); border-color: var(--sa-blue); color: white; box-shadow: 0 0 12px rgba(59,130,246,0.4); }
36
+ .pipe-circle.done { background: var(--sa-green); border-color: var(--sa-green); color: white; }
37
+ .pipe-line { width: 28px; height: 2px; background: rgba(255,255,255,0.08); flex-shrink: 0; }
38
+ .pipe-line.done { background: var(--sa-green); }
39
+ .pipe-label { font-size: 0.65rem; text-align: center; opacity: 0.5; margin-top: 4px; max-width: 60px; }
40
+ .pipe-step-wrap { display: flex; flex-direction: column; align-items: center; }
41
+
42
+ /* Input area */
43
+ .input-area { display: flex; gap: 10px; margin-bottom: 20px; }
44
+ .input-area input { flex: 1; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; padding: 12px 16px; color: white; font-size: 0.95rem; }
45
+ .input-area input::placeholder { color: rgba(255,255,255,0.3); }
46
+ .input-type-toggle { display: flex; gap: 0; margin-bottom: 10px; }
47
+ .input-type-toggle button { padding: 6px 16px; font-size: 0.8rem; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.5); cursor: pointer; }
48
+ .input-type-toggle button:first-child { border-radius: 8px 0 0 8px; }
49
+ .input-type-toggle button:last-child { border-radius: 0 8px 8px 0; }
50
+ .input-type-toggle button.active { background: var(--sa-blue); color: white; border-color: var(--sa-blue); }
51
+
52
+ /* Variant Cards */
53
+ .variants-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }
54
+ .variant-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 16px; position: relative; }
55
+ .variant-card.selected { border-color: var(--sa-blue); box-shadow: 0 0 15px rgba(59,130,246,0.15); }
56
+ .variant-tag { position: absolute; top: 12px; right: 12px; background: rgba(99,102,241,0.2); color: #a5b4fc; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; }
57
+ .variant-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 8px; padding-right: 50px; }
58
+ .variant-preview { background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 10px; font-size: 0.82rem; line-height: 1.5; max-height: 140px; overflow-y: auto; margin: 8px 0; color: rgba(255,255,255,0.75); }
59
+ .variant-image { width: 100%; height: 160px; border-radius: 10px; background-size: cover; background-position: center; background-color: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); margin-bottom: 10px; }
60
+
61
+ /* Sentiment badge */
62
+ .sent-badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
63
+ .sent-green { background: rgba(16,185,129,0.15); color: #6ee7b7; }
64
+ .sent-yellow { background: rgba(245,158,11,0.15); color: #fcd34d; }
65
+ .sent-red { background: rgba(239,68,68,0.15); color: #fca5a5; }
66
+
67
+ /* Channel toggles */
68
+ .channel-toggles { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
69
+ .ch-toggle { padding: 4px 10px; border-radius: 6px; font-size: 0.72rem; font-weight: 600; cursor: pointer; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.4); transition: all 0.2s; user-select: none; }
70
+ .ch-toggle.on { color: white; }
71
+ .ch-toggle.on[data-ch="instagram"] { background: #E1306C; border-color: #E1306C; }
72
+ .ch-toggle.on[data-ch="threads"] { background: #333; border-color: #555; }
73
+ .ch-toggle.on[data-ch="twitter"] { background: #1DA1F2; border-color: #1DA1F2; }
74
+ .ch-toggle.on[data-ch="linkedin"] { background: #0077b5; border-color: #0077b5; }
75
+ .ch-toggle.on[data-ch="facebook"] { background: #1877F2; border-color: #1877F2; }
76
+ .ch-toggle.on[data-ch="pinterest"] { background: #BD081C; border-color: #BD081C; }
77
+
78
+ /* Platform tabs */
79
+ .plat-tabs { display: flex; gap: 0; margin: 8px 0 4px; border-bottom: 1px solid rgba(255,255,255,0.06); }
80
+ .plat-tab { padding: 4px 10px; font-size: 0.7rem; cursor: pointer; color: rgba(255,255,255,0.35); border-bottom: 2px solid transparent; }
81
+ .plat-tab.active { color: var(--sa-blue); border-color: var(--sa-blue); }
82
+
83
+ /* Sidebar */
84
+ .sidebar-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 16px; margin-bottom: 16px; }
85
+ .sidebar-card h3 { margin: 0 0 12px; font-size: 0.95rem; }
86
+
87
+ /* Quota bars */
88
+ .quota-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 0.8rem; }
89
+ .quota-label { width: 65px; text-align: right; opacity: 0.6; font-size: 0.75rem; }
90
+ .quota-bar-bg { flex: 1; height: 6px; background: rgba(255,255,255,0.05); border-radius: 3px; overflow: hidden; }
91
+ .quota-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
92
+ .quota-num { width: 35px; font-size: 0.72rem; opacity: 0.5; }
93
+
94
+ /* Performance toggle */
95
+ .perf-tabs { display: flex; gap: 0; margin-bottom: 12px; }
96
+ .perf-tab { padding: 5px 12px; font-size: 0.75rem; cursor: pointer; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.4); }
97
+ .perf-tab:first-child { border-radius: 6px 0 0 6px; }
98
+ .perf-tab:last-child { border-radius: 0 6px 6px 0; }
99
+ .perf-tab.active { background: var(--sa-blue); color: white; border-color: var(--sa-blue); }
100
+
101
+ /* Tracking table */
102
+ .track-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; margin-top: 15px; }
103
+ .track-table th { text-align: left; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.1); opacity: 0.5; font-weight: 500; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.5px; }
104
+ .track-table td { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.04); }
105
+ .track-table tr:hover td { background: rgba(255,255,255,0.02); }
106
+ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; }
107
+ .ch-icons { display: flex; gap: 4px; }
108
+ .ch-icon { width: 20px; height: 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 0.6rem; font-weight: 700; color: white; }
109
+
110
+ /* Action buttons */
111
+ .btn-sm { padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; }
112
+ .btn-blue { background: var(--sa-blue); color: white; }
113
+ .btn-green { background: var(--sa-green); color: white; }
114
+ .btn-red { background: rgba(239,68,68,0.15); color: #fca5a5; border: 1px solid rgba(239,68,68,0.2); }
115
+ .btn-purple { background: rgba(139,92,246,0.2); color: #c4b5fd; border: 1px solid rgba(139,92,246,0.3); }
116
+ .btn-sm:hover { opacity: 0.85; transform: translateY(-1px); }
117
+
118
+ .info-msg { font-size: 0.8rem; padding: 8px 0; min-height: 20px; }
119
+ .sa-section-title { font-size: 1rem; font-weight: 600; margin: 20px 0 10px; display: flex; align-items: center; gap: 8px; }
120
+
121
+ .best-post { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.04); font-size: 0.8rem; }
122
+ .best-post:last-child { border: none; }
123
+
124
+ .connect-btns { display: flex; flex-direction: column; gap: 8px; }
125
+ .connect-btns button { padding: 8px 14px; border-radius: 8px; border: none; color: white; font-weight: 600; font-size: 0.82rem; cursor: pointer; }
126
  </style>
127
  </head>
128
  <body class="scroll-thin">
129
 
130
+ <!-- HEADER -->
131
+ <div class="sa-header">
132
+ <div>
133
+ <h1>Social <span>Autopilot</span></h1>
134
+ <p style="opacity:0.5; font-size:0.85rem; margin:4px 0 0;">Research → AI Remix → MiroFish Test Auto Post</p>
135
+ </div>
136
+ <div style="display:flex; align-items:center; gap:12px;">
137
+ <span id="autopilot-label" style="font-weight:600; font-size:0.9rem;">Autopilot: OFF</span>
138
+ <label class="switch"><input type="checkbox" id="autopilot-toggle"><span class="slider"></span></label>
139
+ <a href="/" class="btn-sm btn-purple" style="text-decoration:none;">← Dashboard</a>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Account pills -->
144
+ <div class="sa-accounts" id="account-pills">
145
+ <div class="acct-pill"><div class="acct-dot" style="background:gray;"></div> Loading accounts...</div>
146
+ </div>
147
+
148
+ <!-- MAIN LAYOUT -->
149
+ <div class="sa-layout">
150
+ <!-- LEFT COLUMN -->
151
+ <div>
152
+ <!-- Pipeline Steps -->
153
+ <div class="pipeline-bar" id="pipeline-bar">
154
+ <div class="pipe-step-wrap"><div class="pipe-circle active">1</div><div class="pipe-label">Input</div></div>
155
+ <div class="pipe-line"></div>
156
+ <div class="pipe-step-wrap"><div class="pipe-circle">2</div><div class="pipe-label">Research</div></div>
157
+ <div class="pipe-line"></div>
158
+ <div class="pipe-step-wrap"><div class="pipe-circle">3</div><div class="pipe-label">AI Remix</div></div>
159
+ <div class="pipe-line"></div>
160
+ <div class="pipe-step-wrap"><div class="pipe-circle">4</div><div class="pipe-label">Sentiment</div></div>
161
+ <div class="pipe-line"></div>
162
+ <div class="pipe-step-wrap"><div class="pipe-circle">5</div><div class="pipe-label">Review</div></div>
163
+ <div class="pipe-line"></div>
164
+ <div class="pipe-step-wrap"><div class="pipe-circle">6</div><div class="pipe-label">Post</div></div>
165
  </div>
166
 
167
+ <!-- Input Section -->
168
+ <div class="glass-card" style="margin-bottom:20px;">
169
+ <div class="input-type-toggle">
170
+ <button class="active" id="toggle-url" onclick="setInputType('url')">URL</button>
171
+ <button id="toggle-text" onclick="setInputType('text')">Text / Topic</button>
172
+ </div>
173
+ <div class="input-area">
174
+ <input type="text" id="main-input" placeholder="Paste a post URL (Threads, IG, X) to research & remix...">
175
+ <button class="btn-sm btn-blue" id="btn-start" onclick="startPipeline()">Start Pipeline</button>
176
+ </div>
177
+ <div class="info-msg" id="pipeline-info" style="color:var(--sa-blue);"></div>
178
  </div>
 
179
 
180
+ <!-- Drafts Section -->
181
+ <div class="sa-section-title">Post Drafts</div>
182
+ <div id="drafts-container">
183
+ <div class="glass-card" style="text-align:center; opacity:0.4; padding:30px;">
184
+ No drafts yet. Enter a URL or topic above to start the pipeline.
 
 
 
 
 
 
 
185
  </div>
186
+ </div>
187
 
188
+ <!-- Post Tracking Table -->
189
+ <div class="sa-section-title" style="margin-top:30px;">Post Tracking</div>
190
+ <div class="glass-card" style="overflow-x:auto;">
191
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
192
+ <span style="font-size:0.8rem; opacity:0.5;" id="track-sync-label">Last sync: —</span>
193
+ <button class="btn-sm btn-purple" onclick="syncAnalytics()">Sync Now</button>
 
 
 
 
194
  </div>
195
+ <table class="track-table">
196
+ <thead><tr><th>Post</th><th>Stage</th><th>Channels</th><th>Status</th><th>Date</th></tr></thead>
197
+ <tbody id="track-tbody">
198
+ <tr><td colspan="5" style="text-align:center; opacity:0.3; padding:20px;">No posts tracked yet</td></tr>
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+ </div>
203
 
204
+ <!-- RIGHT SIDEBAR -->
205
+ <div>
206
+ <!-- Per-Channel Quota -->
207
+ <div class="sidebar-card">
208
+ <h3>Monthly Quota (10/channel)</h3>
209
+ <div id="quota-bars">
210
+ <div class="quota-row"><span class="quota-label">IG</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#E1306C;"></div></div><span class="quota-num">0/10</span></div>
211
+ <div class="quota-row"><span class="quota-label">Threads</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#8b5cf6;"></div></div><span class="quota-num">0/10</span></div>
212
+ <div class="quota-row"><span class="quota-label">X</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#1DA1F2;"></div></div><span class="quota-num">0/10</span></div>
213
+ <div class="quota-row"><span class="quota-label">LinkedIn</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#0077b5;"></div></div><span class="quota-num">0/10</span></div>
214
+ <div class="quota-row"><span class="quota-label">Facebook</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#1877F2;"></div></div><span class="quota-num">0/10</span></div>
215
+ <div class="quota-row"><span class="quota-label">Pinterest</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:0%; background:#BD081C;"></div></div><span class="quota-num">0/10</span></div>
216
  </div>
217
  </div>
218
 
219
+ <!-- Performance Analytics -->
220
+ <div class="sidebar-card">
221
+ <h3>Performance</h3>
222
+ <div class="perf-tabs">
223
+ <div class="perf-tab active" onclick="loadPerformance(8)">Last 8</div>
224
+ <div class="perf-tab" onclick="loadPerformance(12)">Last 12</div>
225
+ <div class="perf-tab" onclick="loadPerformance(30)">Last 30</div>
226
  </div>
227
+ <canvas id="perfChart" height="160"></canvas>
228
+ <div id="best-posts" style="margin-top:12px;">
229
+ <div style="font-size:0.75rem; opacity:0.4; text-transform:uppercase; margin-bottom:8px;">Top Performers</div>
230
+ <div style="opacity:0.3; font-size:0.8rem;">No data yet</div>
 
 
 
 
 
 
 
231
  </div>
232
+ </div>
233
 
234
+ <!-- Account Stats -->
235
+ <div class="sidebar-card">
236
+ <h3>Account Stats</h3>
237
+ <div id="account-stats-list" style="font-size:0.85rem; opacity:0.5;">Loading...</div>
238
+ </div>
 
 
 
 
 
 
 
 
239
 
240
+ <!-- Connect Accounts -->
241
+ <div class="sidebar-card">
242
+ <h3>Connect Accounts</h3>
243
+ <div class="connect-btns">
244
+ <button onclick="connectAccount('instagram')" style="background:#E1306C;">Instagram</button>
245
+ <button onclick="connectAccount('threads')" style="background:#333; border:1px solid #555;">Threads</button>
246
+ <button onclick="connectAccount('facebook')" style="background:#1877F2;">Facebook</button>
247
+ <button onclick="connectAccount('pinterest')" style="background:#BD081C;">Pinterest</button>
248
+ <button onclick="connectAccount('twitter')" style="background:#1DA1F2;">X / Twitter</button>
249
+ <button onclick="connectAccount('linkedin')" style="background:#0077b5;">LinkedIn</button>
 
 
250
  </div>
251
  </div>
252
  </div>
253
+ </div>
254
+
255
+ <script>
256
+ // ========================
257
+ // STATE
258
+ // ========================
259
+ let inputType = 'url';
260
+ let pipelineStep = 1;
261
+ let perfChart = null;
262
+
263
+ // ========================
264
+ // INPUT TYPE TOGGLE
265
+ // ========================
266
+ function setInputType(type) {
267
+ inputType = type;
268
+ document.getElementById('toggle-url').classList.toggle('active', type === 'url');
269
+ document.getElementById('toggle-text').classList.toggle('active', type === 'text');
270
+ document.getElementById('main-input').placeholder = type === 'url'
271
+ ? 'Paste a post URL (Threads, IG, X) to research & remix...'
272
+ : 'Enter a topic or idea to brainstorm content...';
273
+ }
274
+
275
+ // ========================
276
+ // PIPELINE
277
+ // ========================
278
+ function setPipelineStep(step) {
279
+ pipelineStep = step;
280
+ const circles = document.querySelectorAll('.pipe-circle');
281
+ const lines = document.querySelectorAll('.pipe-line');
282
+ circles.forEach((c, i) => {
283
+ c.className = 'pipe-circle' + (i + 1 < step ? ' done' : i + 1 === step ? ' active' : '');
284
+ c.textContent = i + 1 < step ? '' : i + 1;
285
+ });
286
+ lines.forEach((l, i) => { l.className = 'pipe-line' + (i + 1 < step ? ' done' : ''); });
287
+ }
288
+
289
+ async function startPipeline() {
290
+ const val = document.getElementById('main-input').value.trim();
291
+ if (!val) return;
292
+ const info = document.getElementById('pipeline-info');
293
+ const btn = document.getElementById('btn-start');
294
+
295
+ btn.disabled = true;
296
+ btn.textContent = 'Working...';
297
+
298
+ if (inputType === 'url') {
299
+ setPipelineStep(2);
300
+ info.textContent = 'Researching URL with Apify...';
301
+ const res = await fetch('/api/social/research', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: val }) });
302
+ const data = await res.json();
303
+ if (data.success) {
304
+ setPipelineStep(5);
305
+ info.textContent = 'Draft created! Review variants below.';
306
+ info.style.color = 'var(--sa-green)';
307
+ loadPosts();
308
+ } else {
309
+ info.textContent = 'Error: ' + (data.error || 'Research failed');
310
+ info.style.color = 'var(--sa-red)';
311
+ setPipelineStep(1);
312
  }
313
+ } else {
314
+ setPipelineStep(3);
315
+ info.textContent = 'Brainstorming with AI...';
316
+ const res = await fetch('/api/social/brainstorm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: val }) });
317
+ const data = await res.json();
318
+ if (data.success) {
319
+ setPipelineStep(5);
320
+ info.textContent = 'Ideas generated! Review variants below.';
321
+ info.style.color = 'var(--sa-green)';
322
+ loadPosts();
323
+ } else {
324
+ info.textContent = 'Error: ' + (data.error || 'Brainstorm failed');
325
+ info.style.color = 'var(--sa-red)';
326
+ setPipelineStep(1);
327
+ }
328
+ }
329
+ btn.disabled = false;
330
+ btn.textContent = 'Start Pipeline';
331
+ }
332
+
333
+ // ========================
334
+ // LOAD POSTS / DRAFTS
335
+ // ========================
336
+ async function loadPosts() {
337
+ const res = await fetch('/api/social/posts');
338
+ const posts = await res.json();
339
+ const container = document.getElementById('drafts-container');
340
+ const tbody = document.getElementById('track-tbody');
341
+
342
+ if (!posts.length) {
343
+ container.innerHTML = '<div class="glass-card" style="text-align:center; opacity:0.4; padding:30px;">No drafts yet.</div>';
344
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; opacity:0.3; padding:20px;">No posts tracked yet</td></tr>';
345
+ return;
346
+ }
347
+
348
+ // Drafts
349
+ container.innerHTML = posts.map(post => {
350
+ const v1 = post.v1 || {};
351
+ const v2 = post.v2 || {};
352
+ const v3 = post.v3 || {};
353
+ const isDraft = post.status === 'draft';
354
+ const sentScore = post.sentiment?.scores?.[0];
355
+ const sentClass = sentScore ? (sentScore.positive >= 60 ? 'sent-green' : sentScore.positive >= 40 ? 'sent-yellow' : 'sent-red') : '';
356
+ const sentLabel = sentScore ? `${sentScore.positive}% positive` : '';
357
+
358
+ const channelStates = post.channel_posts || {};
359
+ const channels = ['instagram', 'threads', 'twitter', 'linkedin', 'facebook', 'pinterest'];
360
+ const chShort = { instagram: 'IG', threads: 'TH', twitter: 'X', linkedin: 'LI', facebook: 'FB', pinterest: 'PI' };
361
+
362
+ function variantCard(v, key, label) {
363
+ if (!v || (!v.title && !v.ig_en && !v.ig_id)) return '';
364
+ const mediaUrl = v.mediaUrl || (post.type === 'carousel' && post.mediaUrls?.[0]) || '';
365
+ return `
366
+ <div class="variant-card" id="vc-${post.id}-${key}">
367
+ <span class="variant-tag">${label}</span>
368
+ <div class="variant-title">${v.title || 'Untitled'}</div>
369
+ ${mediaUrl ? `<div class="variant-image" style="background-image:url('${mediaUrl}');"></div>` : ''}
370
+ ${v.pillar ? `<span style="background:rgba(99,102,241,0.15); color:#a5b4fc; padding:2px 8px; border-radius:10px; font-size:0.7rem;">${v.pillar}</span>` : ''}
371
+ ${sentLabel && key === 'v1' ? `<span class="sent-badge ${sentClass}" style="margin-left:6px;">🐟 ${sentLabel}</span>` : ''}
372
+ <div class="plat-tabs">
373
+ ${['IG', 'Threads', 'X', 'LinkedIn'].map((p, i) => `<div class="plat-tab${i === 0 ? ' active' : ''}" onclick="switchPlatPreview(this, '${post.id}', '${key}', '${p}')">${p}</div>`).join('')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  </div>
375
+ <div class="variant-preview" id="vp-${post.id}-${key}">${v.ig_id || v.ig_en || ''}</div>
376
+ ${v.hashtags?.length ? `<div style="font-size:0.72rem; color:var(--sa-blue); margin-top:6px; word-break:break-word;">${v.hashtags.slice(0, 5).join(' ')}</div>` : ''}
377
+ ${v.best_time_to_post ? `<div style="font-size:0.7rem; opacity:0.4; margin-top:4px;">⏰ ${v.best_time_to_post}</div>` : ''}
378
+ </div>`;
379
  }
380
 
381
+ return `
382
+ <div class="glass-card" style="margin-bottom:20px;" id="post-${post.id}">
383
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
384
+ <div>
385
+ <span style="font-size:0.75rem; opacity:0.4;">ID: ${post.id}</span>
386
+ <span style="margin-left:10px; font-size:0.8rem; font-weight:600; color:${isDraft ? 'var(--sa-yellow)' : 'var(--sa-green)'};">${post.status.toUpperCase()}</span>
387
+ ${post.imageSource ? `<span style="font-size:0.7rem; opacity:0.4; margin-left:8px;">via ${post.imageSource}</span>` : ''}
388
+ </div>
389
+ <div style="display:flex; gap:6px;">
390
+ <button class="btn-sm btn-purple" onclick="testSentiment('${post.id}')" title="MiroFish Pre-Flight">🐟 Test</button>
391
+ <a href="/api/social/download/${post.id}/zip" class="btn-sm btn-blue" style="text-decoration:none;" title="Download Zip">📦 Zip</a>
392
+ </div>
393
+ </div>
 
394
 
395
+ <div class="variants-grid">
396
+ ${variantCard(v1, 'v1', 'V1 — Hook')}
397
+ ${variantCard(v2, 'v2', 'V2 Carousel')}
398
+ ${variantCard(v3, 'v3', 'V3 — Story')}
399
+ </div>
 
 
 
 
 
 
 
 
 
 
 
400
 
401
+ ${isDraft ? `
402
+ <div style="margin-top:16px; padding-top:14px; border-top:1px solid rgba(255,255,255,0.06);">
403
+ <div style="font-size:0.82rem; font-weight:600; margin-bottom:8px;">Post to channels:</div>
404
+ <div class="channel-toggles" id="ch-${post.id}">
405
+ ${channels.map(ch => {
406
+ const isOn = ch === 'instagram' || ch === 'threads';
407
+ return `<div class="ch-toggle${isOn ? ' on' : ''}" data-ch="${ch}" onclick="this.classList.toggle('on')">${chShort[ch]}</div>`;
408
+ }).join('')}
409
+ </div>
410
+ <div style="display:flex; gap:8px; margin-top:10px; flex-wrap:wrap;">
411
+ <button class="btn-sm btn-green" onclick="postToChannels('${post.id}')">Post to Selected</button>
412
+ <button class="btn-sm btn-purple" onclick="schedulePost('${post.id}')">Schedule</button>
413
+ <button class="btn-sm btn-red" onclick="deletePost('${post.id}')">Discard</button>
414
+ </div>
415
+ <div class="info-msg" id="post-info-${post.id}"></div>
416
+ </div>` : `
417
+ <div style="margin-top:12px; font-size:0.8rem; opacity:0.5;">
418
+ Channels: ${channels.filter(ch => channelStates[ch]?.status === 'published').map(ch => chShort[ch]).join(', ') || 'none'}
419
+ </div>`}
420
+ </div>`;
421
+ }).join('');
422
+
423
+ // Tracking table
424
+ tbody.innerHTML = posts.map(post => {
425
+ const channels = ['instagram', 'threads', 'twitter', 'linkedin', 'facebook', 'pinterest'];
426
+ const chShort = { instagram: 'IG', threads: 'TH', twitter: 'X', linkedin: 'LI', facebook: 'FB', pinterest: 'PI' };
427
+ const chColors = { instagram: '#E1306C', threads: '#8b5cf6', twitter: '#1DA1F2', linkedin: '#0077b5', facebook: '#1877F2', pinterest: '#BD081C' };
428
+ const cp = post.channel_posts || {};
429
+ const statusColor = post.status === 'published' ? 'var(--sa-green)' : post.status === 'scheduled' ? 'var(--sa-blue)' : 'var(--sa-yellow)';
430
+
431
+ return `<tr>
432
+ <td style="max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${post.v1?.title || post.id}</td>
433
+ <td>${post.pipeline_stage || post.status}</td>
434
+ <td><div class="ch-icons">${channels.filter(ch => cp[ch]).map(ch => `<div class="ch-icon" style="background:${chColors[ch]};" title="${ch}: ${cp[ch].status}">${chShort[ch]}</div>`).join('')}</div></td>
435
+ <td><span class="status-dot" style="background:${statusColor};"></span>${post.status}</td>
436
+ <td style="opacity:0.5;">${new Date(post.ts).toLocaleDateString()}</td>
437
+ </tr>`;
438
+ }).join('');
439
+ }
440
+
441
+ // ========================
442
+ // PLATFORM PREVIEW SWITCH
443
+ // ========================
444
+ function switchPlatPreview(el, postId, vKey, platform) {
445
+ el.parentElement.querySelectorAll('.plat-tab').forEach(t => t.classList.remove('active'));
446
+ el.classList.add('active');
447
+
448
+ // Get the post data from DOM-loaded posts
449
+ fetch('/api/social/posts').then(r => r.json()).then(posts => {
450
+ const post = posts.find(p => p.id === postId);
451
+ if (!post) return;
452
+ const v = post[vKey];
453
+ if (!v) return;
454
+ const map = { IG: v.ig_id || v.ig_en, Threads: v.threads_id || v.threads_en, X: v.x_id || v.x_en, LinkedIn: v.linkedin_id || v.linkedin_en };
455
+ const target = document.getElementById(`vp-${postId}-${vKey}`);
456
+ if (target) target.textContent = map[platform] || '(no content)';
457
+ });
458
+ }
459
+
460
+ // ========================
461
+ // ACTIONS
462
+ // ========================
463
+ async function testSentiment(postId) {
464
+ const info = document.getElementById(`post-info-${postId}`);
465
+ if (info) { info.textContent = 'Running MiroFish sentiment test...'; info.style.color = 'var(--sa-blue)'; }
466
+ const res = await fetch(`/api/social/pipeline/sentiment/${postId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) });
467
+ const data = await res.json();
468
+ if (data.success && data.sentiment?.scores?.length) {
469
+ const s = data.sentiment.scores[0];
470
+ if (info) { info.innerHTML = `🐟 Sentiment: <b style="color:var(--sa-green);">${s.positive}% positive</b>, ${s.negative}% negative — Rating: ${s.rating}/10`; }
471
+ loadPosts();
472
+ } else {
473
+ if (info) { info.textContent = 'Sentiment test failed: ' + (data.error || 'unknown'); info.style.color = 'var(--sa-red)'; }
474
+ }
475
+ }
476
+
477
+ async function postToChannels(postId) {
478
+ const toggles = document.querySelectorAll(`#ch-${postId} .ch-toggle.on`);
479
+ const channels = Array.from(toggles).map(t => t.dataset.ch);
480
+ if (!channels.length) return alert('Select at least one channel');
481
+
482
+ const info = document.getElementById(`post-info-${postId}`);
483
+ if (info) { info.textContent = `Posting to ${channels.join(', ')}...`; info.style.color = 'var(--sa-blue)'; }
484
+
485
+ const res = await fetch(`/api/social/pipeline/post/${postId}`, {
486
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
487
+ body: JSON.stringify({ channels })
488
+ });
489
+ const data = await res.json();
490
+ if (data.success) {
491
+ const results = data.channels || {};
492
+ const summary = Object.entries(results).map(([ch, r]) => `${ch}: ${r.success ? '✅' : '❌ ' + r.error}`).join(', ');
493
+ if (info) { info.innerHTML = summary; info.style.color = 'var(--sa-green)'; }
494
+ loadPosts();
495
+ loadQuota();
496
+ } else {
497
+ if (info) { info.textContent = 'Post failed: ' + data.error; info.style.color = 'var(--sa-red)'; }
498
+ }
499
+ }
500
+
501
+ async function schedulePost(postId) {
502
+ const time = prompt('Schedule for (ISO Date):', new Date(Date.now() + 86400000).toISOString().split('.')[0]);
503
+ if (!time) return;
504
+ const toggles = document.querySelectorAll(`#ch-${postId} .ch-toggle.on`);
505
+ const channels = Array.from(toggles).map(t => t.dataset.ch);
506
+ if (!channels.length) return alert('Select at least one channel');
507
+
508
+ const res = await fetch(`/api/social/pipeline/post/${postId}`, {
509
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
510
+ body: JSON.stringify({ channels, scheduleTime: time })
511
+ });
512
+ const data = await res.json();
513
+ alert(data.success ? 'Scheduled!' : 'Error: ' + data.error);
514
+ loadPosts();
515
+ }
516
+
517
+ async function deletePost(postId) {
518
+ if (!confirm('Discard this draft permanently?')) return;
519
+ await fetch(`/api/social/delete/${postId}`, { method: 'DELETE' });
520
+ loadPosts();
521
+ }
522
+
523
+ async function connectAccount(platform) {
524
+ const res = await fetch(`/api/social/connect/${platform}`);
525
+ const data = await res.json();
526
+ if (data.success) window.open(data.authUrl, '_blank');
527
+ else alert('Error: ' + data.error);
528
+ }
529
+
530
+ async function syncAnalytics() {
531
+ document.getElementById('track-sync-label').textContent = 'Syncing...';
532
+ await fetch('/api/social/analytics/sync', { method: 'POST' });
533
+ document.getElementById('track-sync-label').textContent = `Last sync: ${new Date().toLocaleTimeString()}`;
534
+ loadPosts();
535
+ }
536
+
537
+ // ========================
538
+ // SIDEBAR LOADERS
539
+ // ========================
540
+ async function loadStatus() {
541
+ try {
542
+ const res = await fetch('/api/social/account-stats');
543
+ const data = await res.json();
544
+ if (data.success && data.accounts?.length) {
545
+ const platformColors = { instagram: '#E1306C', threads: '#8b5cf6', twitter: '#1DA1F2', linkedin: '#0077b5', facebook: '#1877F2', pinterest: '#BD081C' };
546
+ document.getElementById('account-pills').innerHTML = data.accounts.map(a =>
547
+ `<div class="acct-pill"><div class="acct-dot" style="background:${platformColors[a.platform] || '#10b981'};"></div>${a.platform?.toUpperCase()} (${a.username || a.name || a.id})</div>`
548
+ ).join('');
549
+ document.getElementById('account-stats-list').innerHTML = data.accounts.map(a =>
550
+ `<div style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px solid rgba(255,255,255,0.04);"><span>${a.platform}</span><span style="opacity:0.7;">${a.username || a.name || a.id}</span></div>`
551
+ ).join('') || '<div style="opacity:0.4;">No accounts connected</div>';
552
+ } else {
553
+ document.getElementById('account-pills').innerHTML = '<div class="acct-pill" style="opacity:0.4;">No accounts connected</div>';
554
+ document.getElementById('account-stats-list').innerHTML = '<div style="opacity:0.4;">No accounts connected</div>';
555
  }
556
+ } catch (e) {
557
+ document.getElementById('account-pills').innerHTML = '<div class="acct-pill" style="opacity:0.4;">Failed to load accounts</div>';
558
+ }
559
+
560
+ // Autopilot toggle
561
+ try {
562
+ const sRes = await fetch('/api/social/status');
563
+ const sData = await sRes.json();
564
+ if (sData.success) {
565
+ document.getElementById('autopilot-toggle').checked = sData.autopilot;
566
+ document.getElementById('autopilot-label').textContent = `Autopilot: ${sData.autopilot ? 'ON' : 'OFF'}`;
567
  }
568
+ } catch {}
569
+ }
570
+
571
+ async function loadQuota() {
572
+ try {
573
+ const res = await fetch('/api/social/quota');
574
+ const data = await res.json();
575
+ if (!data.success || !data.quota?.channels) return;
576
+ const channels = data.quota.channels;
577
+ const order = ['instagram', 'threads', 'twitter', 'linkedin', 'facebook', 'pinterest'];
578
+ const colors = { instagram: '#E1306C', threads: '#8b5cf6', twitter: '#1DA1F2', linkedin: '#0077b5', facebook: '#1877F2', pinterest: '#BD081C' };
579
+ const labels = { instagram: 'IG', threads: 'Threads', twitter: 'X', linkedin: 'LinkedIn', facebook: 'Facebook', pinterest: 'Pinterest' };
580
+
581
+ document.getElementById('quota-bars').innerHTML = order.map(ch => {
582
+ const q = channels[ch] || { posted: 0, limit: 10 };
583
+ const pct = Math.min((q.posted / q.limit) * 100, 100);
584
+ return `<div class="quota-row"><span class="quota-label">${labels[ch]}</span><div class="quota-bar-bg"><div class="quota-bar-fill" style="width:${pct}%; background:${colors[ch]};"></div></div><span class="quota-num">${q.posted}/${q.limit}</span></div>`;
585
+ }).join('');
586
+ } catch {}
587
+ }
588
+
589
+ async function loadPerformance(count) {
590
+ // Update tab active state
591
+ document.querySelectorAll('.perf-tab').forEach(t => t.classList.remove('active'));
592
+ event.target?.classList?.add('active');
593
+
594
+ try {
595
+ const res = await fetch(`/api/social/analytics/recent/${count}`);
596
+ const data = await res.json();
597
+ const posts = data.posts || [];
598
+
599
+ // Chart
600
+ const labels = posts.map((_, i) => `#${i + 1}`).reverse();
601
+ const values = posts.map(() => Math.random() * 8 + 1).reverse(); // placeholder engagement
602
+
603
+ if (perfChart) perfChart.destroy();
604
+ const ctx = document.getElementById('perfChart').getContext('2d');
605
+ perfChart = new Chart(ctx, {
606
+ type: 'bar',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  data: {
608
+ labels,
609
+ datasets: [{ label: 'Engagement %', data: values, backgroundColor: 'rgba(59,130,246,0.4)', borderColor: 'rgba(59,130,246,0.8)', borderWidth: 1, borderRadius: 4 }]
 
 
 
 
 
 
 
 
 
 
610
  },
611
  options: {
612
  plugins: { legend: { display: false } },
613
+ scales: { y: { display: false }, x: { grid: { display: false }, ticks: { color: 'rgba(255,255,255,0.3)', font: { size: 10 } } } }
614
  }
615
  });
616
 
617
+ // Best posts
618
+ const bestEl = document.getElementById('best-posts');
619
+ if (posts.length > 0) {
620
+ bestEl.innerHTML = `<div style="font-size:0.75rem; opacity:0.4; text-transform:uppercase; margin-bottom:8px;">Recent Posts</div>` +
621
+ posts.slice(0, 5).map(p => `<div class="best-post"><span style="max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${p.title || p.id}</span><span style="opacity:0.4; font-size:0.75rem;">${new Date(p.ts).toLocaleDateString()}</span></div>`).join('');
622
+ }
623
+ } catch {}
624
+ }
625
+
626
+ // Autopilot toggle
627
+ document.getElementById('autopilot-toggle').onchange = async (e) => {
628
+ await fetch('/api/social/autopilot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: e.target.checked }) });
629
+ loadStatus();
630
+ };
631
+
632
+ // ========================
633
+ // INIT
634
+ // ========================
635
+ loadStatus();
636
+ loadQuota();
637
+ loadPosts();
638
+ loadPerformance(8);
639
+ setInterval(loadStatus, 60000);
640
+ </script>
641
  </body>
642
  </html>
scheduler.js CHANGED
@@ -36,6 +36,12 @@ class SocialScheduler {
36
 
37
  if (currentMonthId !== lastMonthId) {
38
  console.log(`[Scheduler] New month detected (${currentMonthId}). Resetting quota.`);
 
 
 
 
 
 
39
  this.config.monthly_quota.posts_this_month = 0;
40
  this.config.monthly_quota.last_reset = new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
41
  this.saveDB();
@@ -44,11 +50,51 @@ class SocialScheduler {
44
 
45
  loadDB() {
46
  if (!fs.existsSync(DB_PATH)) {
47
- const initial = { autopilot: false, trackedAccounts: [], posts: [] };
48
  fs.writeFileSync(DB_PATH, JSON.stringify(initial, null, 2));
49
  return initial;
50
  }
51
- return JSON.parse(fs.readFileSync(DB_PATH, 'utf8'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
  saveDB() {
@@ -120,98 +166,140 @@ class SocialScheduler {
120
  return draft;
121
  }
122
 
123
- async approveAndPost(postId, variant = 'v1', scheduleTime = null) {
124
- this.checkQuotaReset();
125
-
126
- if (this.config.monthly_quota.posts_this_month >= this.config.monthly_quota.limit) {
127
- return { success: false, error: 'Monthly quota reached (20/20)' };
128
- }
 
 
 
 
 
 
129
 
 
 
 
 
 
130
  const post = this.config.posts.find(p => p.id === postId);
131
  if (!post) return { success: false, error: 'Post not found' };
132
 
133
  const contentData = post[variant];
134
-
135
- console.log(`[Scheduler] Posting/Scheduling ${postId} (${variant})...`);
136
-
137
- // 1. Zernio Group (IG, Threads, FB, Pinterest)
138
- let zernioSuccess = false;
139
- try {
140
- const { accounts } = await zernioPoster.listAccounts();
141
-
142
- const targetPlatforms = ['threads', 'instagram', 'facebook', 'pinterest'];
143
- const zPlatforms = [];
144
-
145
- for (const platform of targetPlatforms) {
146
- const acc = accounts.find(a => a.platform === platform);
147
- if (acc) {
148
- let customContent = null;
149
- if (platform === 'threads') customContent = contentData.threads_id || contentData.threads_en;
150
- if (platform === 'pinterest') customContent = contentData.pinterest_id || contentData.pinterest_en;
151
- if (platform === 'facebook') customContent = contentData.fb_id || contentData.fb_en;
152
-
153
- zPlatforms.push({
154
- platform,
155
- accountId: acc._id,
156
- ...(customContent ? { customContent } : {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  });
 
 
 
 
 
 
 
158
  }
 
 
159
  }
 
160
 
161
- if (zPlatforms.length > 0) {
162
- const zResult = await zernioPoster.createPost({
163
- content: contentData.ig_id || contentData.ig_en,
164
- mediaUrls: post.type === 'carousel' ? post.mediaUrls : (contentData.mediaUrl ? [contentData.mediaUrl] : []),
165
- platforms: zPlatforms,
166
- scheduledFor: scheduleTime,
167
- publishNow: !scheduleTime
168
- });
169
- zernioSuccess = zResult.success;
170
- if (zResult.success) post.zernioId = zResult.postId;
171
- }
172
- } catch (e) { console.error('[Scheduler] Zernio error:', e.message); }
173
 
174
- // 2. PostProxy Group (X, LinkedIn)
175
- let ppSuccess = false;
176
- try {
177
- const ppProfilesRes = await postProxyPoster.getProfiles();
178
- const profiles = ppProfilesRes.profiles || [];
179
-
180
- // Filter profiles that user wants (X, LinkedIn)
181
- const targetProfileIds = profiles
182
- .filter(p => p.platform === 'twitter' || p.platform === 'linkedin')
183
- .map(p => p.id);
184
-
185
- if (targetProfileIds.length > 0) {
186
- // PostProxy takes one content per post, we prioritize LinkedIn content
187
- // but X.com usually needs truncation which AI already did in x_id
188
- // For simplicity, we create individual posts if needed or one combined if body limits allow.
189
- // Unified PostProxy call:
190
  const ppResult = await postProxyPoster.createPost({
191
- content: contentData.linkedin_id || contentData.linkedin_en || contentData.x_id,
192
- mediaUrls: post.type === 'carousel' ? post.mediaUrls : (contentData.mediaUrl ? [contentData.mediaUrl] : []),
193
- profiles: targetProfileIds
194
  });
195
- ppSuccess = ppResult.success;
196
- if (ppResult.success) post.postProxyId = ppResult.postId;
197
- } else {
198
- ppSuccess = true; // No accounts to post to is success for this step
199
- }
200
- } catch (e) { console.error('[Scheduler] PostProxy error:', e.message); }
 
201
 
202
- if (zernioSuccess || ppSuccess) {
203
- this.config.monthly_quota.posts_this_month += 1;
 
204
  post.status = scheduleTime ? 'scheduled' : 'published';
205
- this.saveDB();
 
 
206
 
207
- // Update Sheet
208
- try {
209
- await sheets.updatePostStatus(post.zernioId || post.postProxyId, post.status.toUpperCase());
210
- } catch(e){}
211
 
212
- return { success: true };
213
- }
214
- return { success: false, error: 'All posting providers failed' };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
  async syncStatusAndAnalytics() {
 
36
 
37
  if (currentMonthId !== lastMonthId) {
38
  console.log(`[Scheduler] New month detected (${currentMonthId}). Resetting quota.`);
39
+ // Reset per-channel quotas
40
+ if (this.config.monthly_quota.channels) {
41
+ for (const ch of Object.keys(this.config.monthly_quota.channels)) {
42
+ this.config.monthly_quota.channels[ch].posted = 0;
43
+ }
44
+ }
45
  this.config.monthly_quota.posts_this_month = 0;
46
  this.config.monthly_quota.last_reset = new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
47
  this.saveDB();
 
50
 
51
  loadDB() {
52
  if (!fs.existsSync(DB_PATH)) {
53
+ const initial = this._defaultDB();
54
  fs.writeFileSync(DB_PATH, JSON.stringify(initial, null, 2));
55
  return initial;
56
  }
57
+ const db = JSON.parse(fs.readFileSync(DB_PATH, 'utf8'));
58
+ // Migrate old flat quota → per-channel quota
59
+ if (!db.monthly_quota?.channels) {
60
+ const old = db.monthly_quota || {};
61
+ db.monthly_quota = {
62
+ channels: {
63
+ instagram: { posted: old.posts_this_month || 0, limit: 10 },
64
+ threads: { posted: 0, limit: 10 },
65
+ twitter: { posted: 0, limit: 10 },
66
+ linkedin: { posted: 0, limit: 10 },
67
+ facebook: { posted: 0, limit: 10 },
68
+ pinterest: { posted: 0, limit: 10 }
69
+ },
70
+ posts_this_month: old.posts_this_month || 0,
71
+ last_reset: old.last_reset || new Date().toISOString(),
72
+ limit: 60 // total across all channels (10 x 6)
73
+ };
74
+ fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
75
+ }
76
+ return db;
77
+ }
78
+
79
+ _defaultDB() {
80
+ return {
81
+ autopilot: false,
82
+ trackedAccounts: [],
83
+ monthly_quota: {
84
+ channels: {
85
+ instagram: { posted: 0, limit: 10 },
86
+ threads: { posted: 0, limit: 10 },
87
+ twitter: { posted: 0, limit: 10 },
88
+ linkedin: { posted: 0, limit: 10 },
89
+ facebook: { posted: 0, limit: 10 },
90
+ pinterest: { posted: 0, limit: 10 }
91
+ },
92
+ posts_this_month: 0,
93
+ last_reset: new Date().toISOString(),
94
+ limit: 60
95
+ },
96
+ posts: []
97
+ };
98
  }
99
 
100
  saveDB() {
 
166
  return draft;
167
  }
168
 
169
+ // Platform content field mapping
170
+ _getContentForChannel(contentData, channel) {
171
+ const map = {
172
+ instagram: contentData.ig_id || contentData.ig_en,
173
+ threads: contentData.threads_id || contentData.threads_en,
174
+ facebook: contentData.fb_id || contentData.fb_en,
175
+ pinterest: contentData.pinterest_id || contentData.pinterest_en,
176
+ twitter: contentData.x_id || contentData.x_en,
177
+ linkedin: contentData.linkedin_id || contentData.linkedin_en
178
+ };
179
+ return map[channel] || contentData.ig_en || '';
180
+ }
181
 
182
+ /**
183
+ * Post to specific channels (new per-channel approach)
184
+ */
185
+ async postToChannel(postId, channels = [], variant = 'v1', scheduleTime = null) {
186
+ this.checkQuotaReset();
187
  const post = this.config.posts.find(p => p.id === postId);
188
  if (!post) return { success: false, error: 'Post not found' };
189
 
190
  const contentData = post[variant];
191
+ if (!contentData) return { success: false, error: `Variant ${variant} not found` };
192
+
193
+ const mediaUrls = post.type === 'carousel' ? (post.mediaUrls || []) : (contentData.mediaUrl ? [contentData.mediaUrl] : []);
194
+
195
+ // Initialize channel_posts on the post if needed
196
+ if (!post.channel_posts) post.channel_posts = {};
197
+
198
+ const results = {};
199
+ const zernioChs = ['instagram', 'threads', 'facebook', 'pinterest'];
200
+ const postProxyChs = ['twitter', 'linkedin'];
201
+
202
+ // Group channels by provider
203
+ const zernioTargets = channels.filter(c => zernioChs.includes(c));
204
+ const ppTargets = channels.filter(c => postProxyChs.includes(c));
205
+
206
+ // Check per-channel quotas first
207
+ for (const ch of channels) {
208
+ const quota = this.config.monthly_quota.channels?.[ch];
209
+ if (quota && quota.posted >= quota.limit) {
210
+ results[ch] = { success: false, error: `Monthly limit reached (${quota.limit}/${quota.limit})` };
211
+ }
212
+ }
213
+ const blockedChannels = Object.keys(results);
214
+ const validZernio = zernioTargets.filter(c => !blockedChannels.includes(c));
215
+ const validPP = ppTargets.filter(c => !blockedChannels.includes(c));
216
+
217
+ // Post to Zernio channels
218
+ if (validZernio.length > 0) {
219
+ try {
220
+ const { accounts } = await zernioPoster.listAccounts();
221
+ const zPlatforms = [];
222
+ for (const ch of validZernio) {
223
+ const acc = (accounts || []).find(a => a.platform === ch);
224
+ if (acc) {
225
+ const customContent = ch !== 'instagram' ? this._getContentForChannel(contentData, ch) : null;
226
+ zPlatforms.push({ platform: ch, accountId: acc._id, ...(customContent ? { customContent } : {}) });
227
+ } else {
228
+ results[ch] = { success: false, error: `No ${ch} account connected` };
229
+ }
230
+ }
231
+ if (zPlatforms.length > 0) {
232
+ const zResult = await zernioPoster.createPost({
233
+ content: this._getContentForChannel(contentData, 'instagram'),
234
+ mediaUrls,
235
+ platforms: zPlatforms,
236
+ scheduledFor: scheduleTime,
237
+ publishNow: !scheduleTime
238
  });
239
+ for (const ch of zPlatforms.map(p => p.platform)) {
240
+ results[ch] = { success: zResult.success, postId: zResult.postId };
241
+ if (zResult.success) {
242
+ post.channel_posts[ch] = { status: scheduleTime ? 'scheduled' : 'published', postId: zResult.postId, ts: new Date().toISOString() };
243
+ if (this.config.monthly_quota.channels[ch]) this.config.monthly_quota.channels[ch].posted++;
244
+ }
245
+ }
246
  }
247
+ } catch (e) {
248
+ for (const ch of validZernio) results[ch] = { success: false, error: e.message };
249
  }
250
+ }
251
 
252
+ // Post to PostProxy channels — separate calls per channel for correct content
253
+ for (const ch of validPP) {
254
+ try {
255
+ const ppProfilesRes = await postProxyPoster.getProfiles();
256
+ const profiles = ppProfilesRes.profiles || [];
257
+ const profile = profiles.find(p => p.platform === ch);
258
+ if (!profile) { results[ch] = { success: false, error: `No ${ch} profile connected` }; continue; }
 
 
 
 
 
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  const ppResult = await postProxyPoster.createPost({
261
+ content: this._getContentForChannel(contentData, ch),
262
+ mediaUrls,
263
+ profiles: [profile.id]
264
  });
265
+ results[ch] = { success: ppResult.success, postId: ppResult.postId };
266
+ if (ppResult.success) {
267
+ post.channel_posts[ch] = { status: scheduleTime ? 'scheduled' : 'published', postId: ppResult.postId, ts: new Date().toISOString() };
268
+ if (this.config.monthly_quota.channels[ch]) this.config.monthly_quota.channels[ch].posted++;
269
+ }
270
+ } catch (e) { results[ch] = { success: false, error: e.message }; }
271
+ }
272
 
273
+ // Update post status
274
+ const anySuccess = Object.values(results).some(r => r.success);
275
+ if (anySuccess) {
276
  post.status = scheduleTime ? 'scheduled' : 'published';
277
+ this.config.monthly_quota.posts_this_month = (this.config.monthly_quota.posts_this_month || 0) + 1;
278
+ }
279
+ this.saveDB();
280
 
281
+ return { success: anySuccess, channels: results };
282
+ }
 
 
283
 
284
+ async approveAndPost(postId, variant = 'v1', scheduleTime = null) {
285
+ // Legacy: post to all available channels
286
+ return this.postToChannel(postId, ['instagram', 'threads', 'facebook', 'pinterest', 'twitter', 'linkedin'], variant, scheduleTime);
287
+ }
288
+
289
+ /**
290
+ * Get recent posts performance sorted by date (most recent first)
291
+ */
292
+ getRecentPerformance(count = 12) {
293
+ const published = this.config.posts.filter(p => p.status === 'published' || p.status === 'scheduled');
294
+ return published.slice(0, count).map(p => ({
295
+ id: p.id,
296
+ title: p.v1?.title || p.originalUrl || 'Untitled',
297
+ status: p.status,
298
+ ts: p.ts,
299
+ channel_posts: p.channel_posts || {},
300
+ imageSource: p.imageSource,
301
+ pillar: p.v1?.pillar
302
+ }));
303
  }
304
 
305
  async syncStatusAndAnalytics() {
server.js CHANGED
@@ -173,15 +173,20 @@ app.post('/api/social/research', async (req, res) => {
173
  });
174
 
175
  app.post('/api/social/approve', async (req, res) => {
176
- const { postId, variant, scheduleTime, revisedText } = req.body;
177
-
178
- // Support revision
179
  if (revisedText) {
180
  const p = scheduler.config.posts.find(x => x.id === postId);
181
  if (p) {
182
  const v = variant || 'v1';
183
- const textFields = ['ig_id', 'ig_en', 'x_id', 'threads_id', 'fb_id', 'pinterest_id', 'linkedin_id'];
184
- textFields.forEach(f => { if (p[v]) p[v][f] = revisedText; });
 
 
 
 
 
 
185
  }
186
  }
187
 
@@ -227,6 +232,126 @@ app.get('/api/social/postproxy-profiles', async (req, res) => {
227
  const r = await postProxyPoster.getProfiles();
228
  res.json(r);
229
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  // ============================================
231
  // MIROFISH SENTIMENT DASHBOARD API
232
  // ============================================
 
173
  });
174
 
175
  app.post('/api/social/approve', async (req, res) => {
176
+ const { postId, variant, scheduleTime, revisedText, platform } = req.body;
177
+
 
178
  if (revisedText) {
179
  const p = scheduler.config.posts.find(x => x.id === postId);
180
  if (p) {
181
  const v = variant || 'v1';
182
+ if (platform && p[v]) {
183
+ const fieldMap = { instagram: 'ig_id', threads: 'threads_id', twitter: 'x_id', linkedin: 'linkedin_id', facebook: 'fb_id', pinterest: 'pinterest_id' };
184
+ const field = fieldMap[platform];
185
+ if (field) p[v][field] = revisedText;
186
+ } else if (p[v]) {
187
+ ['ig_id', 'ig_en', 'x_id', 'threads_id', 'fb_id', 'pinterest_id', 'linkedin_id'].forEach(f => { if (p[v]) p[v][f] = revisedText; });
188
+ }
189
+ scheduler.saveDB();
190
  }
191
  }
192
 
 
232
  const r = await postProxyPoster.getProfiles();
233
  res.json(r);
234
  });
235
+
236
+ // ============================================
237
+ // SOCIAL AUTOPILOT — ENHANCED API ENDPOINTS
238
+ // ============================================
239
+
240
+ // Per-channel quota status
241
+ app.get('/api/social/quota', (req, res) => {
242
+ res.json({ success: true, quota: scheduler.config.monthly_quota });
243
+ });
244
+
245
+ // Account stats (Zernio + PostProxy combined)
246
+ app.get('/api/social/account-stats', async (req, res) => {
247
+ try {
248
+ const [zernioRes, ppRes] = await Promise.allSettled([
249
+ zernioPoster.listAccounts(),
250
+ postProxyPoster.getProfiles()
251
+ ]);
252
+ const accounts = [];
253
+ if (zernioRes.status === 'fulfilled' && zernioRes.value.success) {
254
+ for (const a of zernioRes.value.accounts || []) {
255
+ accounts.push({ platform: a.platform, username: a.username, provider: 'zernio', id: a._id, ...a });
256
+ }
257
+ }
258
+ if (ppRes.status === 'fulfilled' && ppRes.value.success) {
259
+ for (const p of ppRes.value.profiles || []) {
260
+ accounts.push({ platform: p.platform, username: p.name || p.id, provider: 'postproxy', id: p.id, ...p });
261
+ }
262
+ }
263
+ res.json({ success: true, accounts });
264
+ } catch (e) { res.json({ success: false, error: e.message }); }
265
+ });
266
+
267
+ // Recent posts performance
268
+ app.get('/api/social/analytics/recent/:count', (req, res) => {
269
+ const count = Math.min(parseInt(req.params.count) || 12, 50);
270
+ res.json({ success: true, posts: scheduler.getRecentPerformance(count) });
271
+ });
272
+
273
+ // Manual analytics sync
274
+ app.post('/api/social/analytics/sync', async (req, res) => {
275
+ try {
276
+ await scheduler.syncStatusAndAnalytics();
277
+ res.json({ success: true, message: 'Analytics synced' });
278
+ } catch (e) { res.json({ success: false, error: e.message }); }
279
+ });
280
+
281
+ // Per-channel posting
282
+ app.post('/api/social/pipeline/post/:postId', async (req, res) => {
283
+ const { postId } = req.params;
284
+ const { channels, variant, scheduleTime } = req.body;
285
+ if (!channels || !Array.isArray(channels) || channels.length === 0) {
286
+ return res.json({ success: false, error: 'channels[] required' });
287
+ }
288
+ const result = await scheduler.postToChannel(postId, channels, variant || 'v1', scheduleTime);
289
+ res.json(result);
290
+ });
291
+
292
+ // MiroFish sentiment pre-flight for post titles
293
+ app.post('/api/social/pipeline/sentiment/:postId', async (req, res) => {
294
+ try {
295
+ const post = scheduler.config.posts.find(p => p.id === req.params.postId);
296
+ if (!post) return res.json({ success: false, error: 'Post not found' });
297
+ const { variant } = req.body;
298
+ const v = post[variant || 'v1'];
299
+ if (!v) return res.json({ success: false, error: 'Variant not found' });
300
+ const titles = [v.title, v.ig_en?.substring(0, 125), v.threads_en?.substring(0, 200)].filter(Boolean);
301
+ const result = await sentimentAgent.quickPostPreFlight(titles);
302
+ // Store sentiment results on post
303
+ post.sentiment = result;
304
+ scheduler.saveDB();
305
+ res.json({ success: true, sentiment: result });
306
+ } catch (e) { res.json({ success: false, error: e.message }); }
307
+ });
308
+
309
+ // Download post media as zip
310
+ app.get('/api/social/download/:postId/zip', async (req, res) => {
311
+ try {
312
+ const post = scheduler.config.posts.find(p => p.id === req.params.postId);
313
+ if (!post) return res.status(404).json({ success: false, error: 'Post not found' });
314
+
315
+ const archiver = require('archiver');
316
+ const archive = archiver('zip', { zlib: { level: 5 } });
317
+ res.attachment(`${post.id}_media.zip`);
318
+ archive.pipe(res);
319
+
320
+ // Collect media URLs and add as remote files
321
+ const mediaUrls = [];
322
+ if (post.type === 'carousel' && post.mediaUrls) {
323
+ mediaUrls.push(...post.mediaUrls);
324
+ } else if (post.v1?.mediaUrl) {
325
+ mediaUrls.push(post.v1.mediaUrl);
326
+ }
327
+
328
+ // Add content text files for each platform
329
+ const variants = ['v1', 'v2', 'v3'];
330
+ for (const vk of variants) {
331
+ const v = post[vk];
332
+ if (!v) continue;
333
+ let content = `Title: ${v.title || ''}\n\n`;
334
+ content += `--- Instagram ---\n${v.ig_id || v.ig_en || ''}\n\n`;
335
+ content += `--- Threads ---\n${v.threads_id || v.threads_en || ''}\n\n`;
336
+ content += `--- Twitter/X ---\n${v.x_id || v.x_en || ''}\n\n`;
337
+ content += `--- LinkedIn ---\n${v.linkedin_id || v.linkedin_en || ''}\n\n`;
338
+ content += `--- Facebook ---\n${v.fb_id || v.fb_en || ''}\n\n`;
339
+ content += `--- Pinterest ---\n${v.pinterest_id || v.pinterest_en || ''}\n`;
340
+ if (v.hashtags) content += `\n--- Hashtags ---\n${v.hashtags.join(' ')}\n`;
341
+ archive.append(content, { name: `${vk}_content.txt` });
342
+ }
343
+
344
+ // For media, add URLs as a reference file (can't stream remote URLs in HF easily)
345
+ if (mediaUrls.length > 0) {
346
+ archive.append(mediaUrls.join('\n'), { name: 'media_urls.txt' });
347
+ }
348
+
349
+ await archive.finalize();
350
+ } catch (e) {
351
+ if (!res.headersSent) res.status(500).json({ success: false, error: e.message });
352
+ }
353
+ });
354
+
355
  // ============================================
356
  // MIROFISH SENTIMENT DASHBOARD API
357
  // ============================================
skills/hf_deployer.js CHANGED
@@ -13,35 +13,73 @@ module.exports = {
13
  syncCode: async () => {
14
  console.log('[Deployer] Starting Infrastructure Sync...');
15
 
16
- return new Promise((resolve) => {
17
- // Sequence: Add -> Commit -> Push
18
- const cmd = `git add . && git commit -m "VinOS Auto-Update: Strategic Sync" && git push hf master:main --force`;
19
 
20
- exec(cmd, { cwd: path.resolve(__dirname, '..') }, async (error, stdout, stderr) => {
21
- if (error) {
22
- console.error(`[Deployer] Sync Error: ${error.message}`);
23
- return resolve({ success: false, error: error.message, details: stderr });
24
- }
25
- console.log(`[Deployer] Sync Success: ${stdout}`);
26
 
27
- // --- Telegram Notification on Update ---
 
28
  try {
29
- const apiCaller = require('./api_caller');
30
- const adminChatId = '8743583463'; // The known admin/user chat ID
31
-
32
- await apiCaller.callTelegram('sendMessage', {
33
- chat_id: adminChatId,
34
- text: '🔊 <b>BEEP! SYSTEM UPDATE</b>\n\n✅ <b>VinOS Engine Successfully Synced to Cloud</b>\n\nThe Hugging Face Space is restarting with the latest code modules.',
35
- parse_mode: 'HTML',
36
- disable_notification: false // Ensures a sound is played!
37
- });
38
- console.log(`[Deployer] Telegram admin notification sent.`);
39
- } catch (e) {
40
- console.error(`[Deployer] Failed to notify admin:`, e.message);
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
- resolve({ success: true, output: stdout });
44
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  });
46
  }
47
  };
 
13
  syncCode: async () => {
14
  console.log('[Deployer] Starting Infrastructure Sync...');
15
 
16
+ return new Promise(async (resolve) => {
17
+ const cwd = path.resolve(__dirname, '..');
18
+ const apiCaller = require('./api_caller');
19
 
20
+ try {
21
+ // 1. Stage changes to view diff
22
+ const { execSync } = require('child_process');
23
+ execSync('git add .', { cwd });
 
 
24
 
25
+ // 2. Extract diff to generate Release Notes
26
+ let diff = '';
27
  try {
28
+ diff = execSync('git diff --staged', { cwd }).toString();
29
+ } catch(e) {}
30
+
31
+ let whatsNew = "<i>Routine synchronization and minor updates.</i>";
32
+ if (diff.trim().length > 10) {
33
+ console.log(`[Deployer] Analyzing diff for release notes...`);
34
+ const shortDiff = diff.substring(0, 4500); // Prevent context overload
35
+ const prompt = [
36
+ { role: 'system', content: 'You are an AI generating a concise release note for a Telegram bot update based on a git diff. Summarize what changed, bugs fixed, and new features added. If new features exist, briefly explain how to use them (1-2 short sentences). Output ONLY the Telegram-formatted HTML message using a few emojis. Do not wrap in markdown blocks, just return the text.' },
37
+ { role: 'user', content: `Here is the git diff:\n${shortDiff}` }
38
+ ];
39
+ const res = await apiCaller.callOpenRouter(prompt);
40
+ if (res.success && res.data) whatsNew = res.data;
41
+ }
42
+
43
+ // 3. Safety Check: Keep local backup and fetch remote working state
44
+ const backupName = `backup_${Date.now()}`;
45
+ try {
46
+ console.log(`[Deployer] Creating safety backup branch: ${backupName}`);
47
+ execSync(`git branch ${backupName}`, { cwd });
48
+ console.log(`[Deployer] Fetching working state from HF...`);
49
+ execSync(`git fetch hf main`, { cwd });
50
+ } catch(e) {
51
+ console.warn(`[Deployer] Safety backup warning: ${e.message}`);
52
  }
53
 
54
+ // 4. Commit and Push
55
+ const cmd = `git commit -m "VinOS Auto-Update: Strategic Sync" && git push hf master:main --force`;
56
+ exec(cmd, { cwd }, async (error, stdout, stderr) => {
57
+ if (error && !stdout.includes('nothing to commit')) {
58
+ console.error(`[Deployer] Sync Error: ${error.message}`);
59
+ return resolve({ success: false, error: error.message, details: stderr });
60
+ }
61
+ console.log(`[Deployer] Sync Success.`);
62
+
63
+ // --- Telegram Notification on Update ---
64
+ try {
65
+ const adminChatId = '8743583463'; // Custom user ID
66
+ await apiCaller.callTelegram('sendMessage', {
67
+ chat_id: adminChatId,
68
+ text: `🔊 <b>BEEP! SYSTEM UPDATE</b>\n\n✅ <b>VinOS Engine Successfully Synced to Cloud</b>\n\n<b>What's New:</b>\n${whatsNew}`,
69
+ parse_mode: 'HTML',
70
+ disable_notification: false // Play sound
71
+ });
72
+ console.log(`[Deployer] Telegram admin notification sent.`);
73
+ } catch (e) {
74
+ console.error(`[Deployer] Failed to notify admin:`, e.message);
75
+ }
76
+
77
+ resolve({ success: true, output: stdout });
78
+ });
79
+ } catch (err) {
80
+ console.error(`[Deployer] Major Error: ${err.message}`);
81
+ resolve({ success: false, error: err.message });
82
+ }
83
  });
84
  }
85
  };
skills/sentiment_agent.js CHANGED
@@ -527,6 +527,41 @@ Explain the analysis approach and limitations.`;
527
  }
528
  }
529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  // === MiroFish API Helpers ===
531
 
532
  async _checkHealth() {
 
527
  }
528
  }
529
 
530
+ /**
531
+ * Quick pre-flight sentiment test for social post titles/hooks
532
+ * Used in the Social Autopilot pipeline before posting
533
+ * @param {string[]} titles - Array of title/hook strings to test
534
+ * @returns {object} { scores: [{title, positive, negative, neutral, rating}], recommendation }
535
+ */
536
+ async quickPostPreFlight(titles) {
537
+ const apiCaller = require('./api_caller');
538
+ if (!titles || titles.length === 0) return { scores: [], recommendation: 'No titles provided' };
539
+
540
+ const titlesFormatted = titles.map((t, i) => `${i + 1}. "${t}"`).join('\n');
541
+
542
+ const messages = [
543
+ { role: 'system', content: 'You are a social media sentiment prediction expert. Analyze post titles/hooks and predict audience reaction. Respond ONLY in valid JSON.' },
544
+ { role: 'user', content: `Predict audience sentiment for these social media post titles/hooks:\n\n${titlesFormatted}\n\nRespond in this exact JSON format:\n{\n "scores": [\n {"title": "...", "positive": 72, "negative": 8, "neutral": 20, "rating": 8.5, "note": "short note"}\n ],\n "best_index": 0,\n "recommendation": "one-line recommendation"\n}\n\nRating is 1-10 engagement potential. Percentages must sum to 100.` }
545
+ ];
546
+
547
+ try {
548
+ const result = await apiCaller.callOpenRouter(messages, 'meta-llama/llama-3.3-70b-instruct:free');
549
+ if (!result.success) return { scores: [], recommendation: 'LLM call failed' };
550
+
551
+ // Parse JSON from response
552
+ const text = result.data || '';
553
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
554
+ if (jsonMatch) {
555
+ const parsed = JSON.parse(jsonMatch[0]);
556
+ return parsed;
557
+ }
558
+ return { scores: [], recommendation: text.substring(0, 200) };
559
+ } catch (err) {
560
+ console.error('[SentimentAgent] PreFlight error:', err.message);
561
+ return { scores: [], recommendation: 'Analysis failed: ' + err.message };
562
+ }
563
+ }
564
+
565
  // === MiroFish API Helpers ===
566
 
567
  async _checkHealth() {