diff --git a/db/migrations/001_init.sql b/db/migrations/001_init.sql index 6340350..769e763 100644 --- a/db/migrations/001_init.sql +++ b/db/migrations/001_init.sql @@ -331,3 +331,35 @@ CREATE UNIQUE INDEX idx_proposal_submissions_user_payload_prepared CREATE UNIQUE INDEX idx_proposal_submissions_governance_hash ON proposal_submissions(governance_hash) WHERE governance_hash IS NOT NULL; + +-- masternode_count_daily: historical time series of total masternode +-- count, one row per UTC calendar day, feeding the node-count trend +-- chart on /mnCount (TrendChart.js). Previously this data lived in +-- an out-of-repo CSV at /root/sysnode/data.csv that was maintained +-- by an unsupervised standalone script (mnCount.js) using hardcoded +-- RPC credentials; the reader (routes/csvParser.js) opened the file +-- directly, which broke on any host where the daemon hadn't been set +-- up with root permissions (staging, fresh deploys, self-hosted +-- mirrors). Moving the store into SQLite eliminates a whole class of +-- deployment bugs (wrong cwd, missing file, EACCES, crontab-not- +-- installed) and makes the writer part of the normal pm2-managed +-- backend lifecycle. +-- +-- Shape: +-- date 'YYYY-MM-DD' (UTC). PRIMARY KEY so INSERT OR IGNORE +-- is the idempotent write — a restart-storm cannot +-- produce duplicate rows for the same day. +-- total snapshot of masternode_count.total at sample time. +-- recorded_at epoch ms when we actually sampled (for audit; the +-- chart uses `date`, not this value). +-- +-- Seeding: the 2018-05-15 → 2026-04-22 historical series from the +-- legacy CSV is loaded once on first boot by lib/mnCountSeed.js from +-- db/seeds/masternode-count.csv (idempotent — runs only when the +-- table is empty). The writer (services/mnCountLogger.js) takes +-- over from there, appending one row per UTC midnight. +CREATE TABLE masternode_count_daily ( + date TEXT PRIMARY KEY NOT NULL, + total INTEGER NOT NULL, + recorded_at INTEGER NOT NULL +); diff --git a/db/seeds/masternode-count.csv b/db/seeds/masternode-count.csv new file mode 100644 index 0000000..867d189 --- /dev/null +++ b/db/seeds/masternode-count.csv @@ -0,0 +1,2885 @@ +Timestamp;Amount +1526425200000;820 +1526511600000;823 +1526598000000;834 +1526684400000;843 +1526770800000;886 +1526857200000;898 +1526943600000;908 +1527030000000;921 +1527116400000;953 +1527202800000;965 +1527289200000;965 +1527375600000;979 +1527462000000;985 +1527548400000;997 +1527634800000;999 +1527721200000;1016 +1527807600000;1031 +1527894000000;1036 +1527980400000;1040 +1528066800000;1043 +1528153200000;1043 +1528239600000;1035 +1528326000000;1040 +1528412400000;1042 +1528498800000;1046 +1528585200000;1035 +1528671600000;1039 +1528758000000;1052 +1528844400000;1053 +1528930800000;1056 +1529017200000;1052 +1529103600000;1060 +1529190000000;1066 +1529276400000;1063 +1529362800000;1085 +1529449200000;1094 +1529535600000;1107 +1529622000000;1114 +1529708400000;1114 +1529794800000;1122 +1529881200000;1125 +1529967600000;1122 +1530054000000;1099 +1530140400000;1119 +1530226800000;1133 +1530313200000;1138 +1530399600000;1136 +1530486000000;1151 +1530572400000;733 +1530658800000;830 +1530745200000;914 +1530831600000;964 +1530918000000;983 +1531004400000;1046 +1531090800000;1063 +1531177200000;1075 +1531263600000;1092 +1531350000000;1103 +1531436400000;1108 +1531522800000;1109 +1531609200000;1110 +1531695600000;1116 +1531782000000;1122 +1531868400000;1141 +1531954800000;1153 +1532041200000;1162 +1532127600000;1156 +1532214000000;1154 +1532300400000;1159 +1532386800000;1163 +1532473200000;1166 +1532559600000;1169 +1532646000000;1159 +1532732400000;1163 +1532818800000;1171 +1532905200000;1175 +1532991600000;1173 +1533078000000;1177 +1533164400000;1182 +1533250800000;1185 +1533337200000;1191 +1533423600000;1199 +1533510000000;1200 +1533596400000;1194 +1533682800000;1200 +1533769200000;1204 +1533855600000;1205 +1533942000000;1216 +1534028400000;1211 +1534114800000;1213 +1534201200000;1213 +1534287600000;1211 +1534374000000;1207 +1534460400000;1212 +1534546800000;1211 +1534633200000;1213 +1534719600000;1213 +1534806000000;1217 +1534892400000;1217 +1534978800000;1217 +1535065200000;1203 +1535151600000;1234 +1535238000000;1229 +1535324400000;1232 +1535410800000;1233 +1535497200000;1230 +1535583600000;1241 +1535670000000;1234 +1535756400000;1230 +1535842800000;1230 +1535929200000;1248 +1536015600000;1246 +1536102000000;1067 +1536188400000;1058 +1536274800000;1085 +1536361200000;1092 +1536447600000;1105 +1536534000000;1113 +1536620400000;1123 +1536706800000;1139 +1536793200000;1153 +1536879600000;1157 +1536966000000;1161 +1537052400000;1168 +1537138800000;1177 +1537225200000;1172 +1537311600000;1179 +1537398000000;1204 +1537484400000;1223 +1537570800000;1233 +1537657200000;1212 +1537743600000;1232 +1537830000000;1246 +1537916400000;1247 +1538002800000;1251 +1538089200000;1253 +1538175600000;1256 +1538262000000;1257 +1538348400000;1252 +1538434800000;1250 +1538521200000;1254 +1538607600000;1253 +1538694000000;1257 +1538780400000;1260 +1538866800000;1256 +1538953200000;1248 +1539039600000;1264 +1539126000000;1263 +1539212400000;1271 +1539298800000;1270 +1539385200000;1268 +1539471600000;1265 +1539558000000;1266 +1539644400000;1275 +1539730800000;1276 +1539817200000;1274 +1539903600000;1278 +1539990000000;1292 +1540076400000;1293 +1540162800000;1290 +1540249200000;1307 +1540335600000;1314 +1540422000000;1321 +1540508400000;1317 +1540594800000;1331 +1540681200000;1327 +1540771200000;1331 +1540857600000;1337 +1540944000000;1354 +1541030400000;1354 +1541116800000;1356 +1541203200000;1371 +1541289600000;1372 +1541376000000;1373 +1541462400000;1373 +1541548800000;1374 +1541635200000;1371 +1541721600000;1372 +1541808000000;1372 +1541894400000;1374 +1541980800000;1376 +1542067200000;1368 +1542153600000;1365 +1542240000000;1367 +1542326400000;1366 +1542412800000;1363 +1542499200000;1361 +1542585600000;1357 +1542672000000;1354 +1542758400000;1349 +1542844800000;1348 +1542931200000;1346 +1543017600000;1343 +1543104000000;1338 +1543190400000;1339 +1543276800000;1331 +1543363200000;1326 +1543449600000;1316 +1543536000000;1314 +1543622400000;1315 +1543708800000;1313 +1543795200000;1326 +1543881600000;1330 +1543968000000;1330 +1544054400000;1355 +1544140800000;1351 +1544227200000;1342 +1544313600000;1342 +1544400000000;1345 +1544486400000;1343 +1544572800000;1345 +1544659200000;1341 +1544745600000;1334 +1544832000000;1330 +1544918400000;1333 +1545004800000;1331 +1545091200000;1327 +1545177600000;1325 +1545264000000;1314 +1545350400000;1317 +1545436800000;1320 +1545523200000;1353 +1545609600000;1355 +1545696000000;1357 +1545782400000;1361 +1545868800000;1359 +1545955200000;1358 +1546041600000;1358 +1546128000000;1361 +1546214400000;1362 +1546300800000;1363 +1546387200000;1361 +1546473600000;1367 +1546560000000;1366 +1546646400000;1364 +1546732800000;1367 +1546819200000;1370 +1546905600000;1370 +1546992000000;1367 +1547078400000;1376 +1547164800000;1379 +1547251200000;1383 +1547337600000;1383 +1547424000000;1391 +1547510400000;1362 +1547596800000;1365 +1547683200000;1365 +1547769600000;1369 +1547856000000;1388 +1547942400000;1388 +1548028800000;1391 +1548115200000;1391 +1548201600000;1372 +1548288000000;1388 +1548374400000;1247 +1548460800000;1362 +1548547200000;1366 +1548633600000;1347 +1548720000000;1367 +1548806400000;1385 +1548892800000;1383 +1548979200000;1379 +1549065600000;1381 +1549152000000;1379 +1549238400000;1378 +1549324800000;1381 +1549411200000;1377 +1549497600000;1381 +1549584000000;1377 +1549670400000;1378 +1549756800000;1356 +1549843200000;1354 +1549929600000;1347 +1550016000000;1345 +1550102400000;1345 +1550188800000;1347 +1550275200000;1367 +1550361600000;1371 +1550448000000;1350 +1550534400000;1349 +1550620800000;1347 +1550707200000;1340 +1550793600000;1350 +1550880000000;1353 +1550966400000;1372 +1551052800000;1374 +1551139200000;1376 +1551225600000;1376 +1551312000000;1355 +1551398400000;1360 +1551484800000;1363 +1551571200000;1359 +1551657600000;1355 +1551744000000;1354 +1551830400000;1354 +1551916800000;1352 +1552003200000;1352 +1552089600000;1348 +1552176000000;1346 +1552262400000;1347 +1552348800000;1346 +1552435200000;1346 +1552521600000;1342 +1552608000000;1339 +1552694400000;1339 +1552780800000;1341 +1552867200000;1346 +1552953600000;1344 +1553040000000;1349 +1553126400000;1350 +1553212800000;1351 +1553299200000;1352 +1553385600000;1354 +1553472000000;1360 +1553558400000;1366 +1553644800000;1362 +1553731200000;1385 +1553817600000;1401 +1553904000000;1424 +1553990400000;1421 +1554073200000;1445 +1554159600000;1471 +1554246000000;1461 +1554332400000;1492 +1554418800000;1503 +1554505200000;1514 +1554591600000;1512 +1554678000000;1518 +1554764400000;1504 +1554850800000;1531 +1554937200000;1528 +1555023600000;1529 +1555110000000;1527 +1555196400000;1527 +1555282800000;1525 +1555369200000;1532 +1555455600000;1545 +1555542000000;1545 +1555628400000;1543 +1555714800000;1543 +1555801200000;1542 +1555887600000;1542 +1555974000000;1541 +1556060400000;1549 +1556146800000;1548 +1556233200000;1546 +1556319600000;1553 +1556406000000;1554 +1556492400000;1552 +1556578800000;1551 +1556665200000;1552 +1556751600000;1521 +1556838000000;1519 +1556924400000;1511 +1557010800000;1509 +1557097200000;1512 +1557183600000;1512 +1557270000000;1511 +1557356400000;1508 +1557442800000;1511 +1557529200000;1510 +1557615600000;1508 +1557702000000;1511 +1557788400000;1506 +1557874800000;1503 +1557961200000;1499 +1558047600000;1486 +1558134000000;1487 +1558220400000;1492 +1558306800000;1488 +1558393200000;1473 +1558479600000;1477 +1558566000000;1477 +1558652400000;1482 +1558738800000;1468 +1558825200000;1464 +1558911600000;1497 +1558998000000;1492 +1559084400000;1486 +1559170800000;1475 +1559257200000;1415 +1559343600000;1442 +1559430000000;1409 +1559516400000;1378 +1559602800000;1130 +1559689200000;881 +1559775600000;504 +1559862000000;608 +1559948400000;662 +1560034800000;744 +1560121200000;838 +1560207600000;879 +1560294000000;926 +1560380400000;971 +1560466800000;990 +1560553200000;994 +1560639600000;1015 +1560726000000;1016 +1560812400000;1034 +1560898800000;1032 +1560985200000;1045 +1561071600000;1074 +1561158000000;1087 +1561244400000;1094 +1561330800000;1106 +1561417200000;1116 +1561503600000;1160 +1561590000000;1161 +1561676400000;1165 +1561762800000;1174 +1561849200000;1178 +1561935600000;1186 +1562022000000;1231 +1562108400000;1251 +1562194800000;1281 +1562281200000;1278 +1562367600000;1268 +1562454000000;1402 +1562540400000;1417 +1562626800000;1417 +1562713200000;1421 +1562799600000;1440 +1562886000000;1428 +1562972400000;1429 +1563058800000;1431 +1563145200000;1429 +1563231600000;1436 +1563318000000;1504 +1563404400000;1511 +1563490800000;1515 +1563577200000;1519 +1563663600000;1514 +1563750000000;1516 +1563836400000;1525 +1563922800000;1527 +1564009200000;1523 +1564095600000;1523 +1564182000000;1524 +1564268400000;1509 +1564354800000;1527 +1564441200000;1526 +1564527600000;1536 +1564614000000;1532 +1564700400000;1533 +1564786800000;1530 +1564873200000;1505 +1564959600000;1517 +1565046000000;1529 +1565132400000;1531 +1565218800000;1532 +1565305200000;1534 +1565391600000;1535 +1565478000000;1522 +1565564400000;1540 +1565650800000;1532 +1565737200000;1535 +1565823600000;1545 +1565910000000;1539 +1565996400000;1535 +1566082800000;1530 +1566169200000;1520 +1566255600000;1540 +1566342000000;1545 +1566428400000;1547 +1566514800000;1544 +1566601200000;1557 +1566687600000;1549 +1566774000000;1547 +1566860400000;1551 +1566946800000;1558 +1567033200000;1554 +1567119600000;1555 +1567206000000;1554 +1567292400000;1548 +1567378800000;1531 +1567465200000;1538 +1567551600000;1545 +1567638000000;1547 +1567724400000;1547 +1567810800000;1549 +1567897200000;1543 +1567983600000;1542 +1568070000000;1541 +1568156400000;1544 +1568242800000;1543 +1568329200000;1542 +1568415600000;1534 +1568502000000;1533 +1568588400000;1526 +1568674800000;1542 +1568761200000;1538 +1568847600000;1547 +1568934000000;1553 +1569020400000;1553 +1569106800000;1560 +1569193200000;1558 +1569279600000;1557 +1569366000000;1561 +1569452400000;1582 +1569538800000;1584 +1569625200000;1601 +1569711600000;1597 +1569798000000;1607 +1569884400000;1639 +1569970800000;1629 +1570057200000;1622 +1570143600000;1618 +1570230000000;1618 +1570316400000;1622 +1570402800000;1622 +1570489200000;1624 +1570575600000;1631 +1570662000000;1641 +1570748400000;1648 +1570834800000;1652 +1570921200000;1647 +1571007600000;1646 +1571094000000;1642 +1571180400000;1650 +1571266800000;1651 +1571353200000;1657 +1571439600000;1649 +1571526000000;1652 +1571612400000;1655 +1571698800000;1648 +1571785200000;1629 +1571871600000;1645 +1571958000000;1642 +1572044400000;1643 +1572130800000;1643 +1572220800000;1642 +1572307200000;1640 +1572393600000;1641 +1572480000000;1617 +1572566400000;1647 +1572652800000;1628 +1572739200000;1628 +1572825600000;1635 +1572912000000;1630 +1572998400000;1627 +1573084800000;1623 +1573171200000;1594 +1573257600000;1594 +1573344000000;1637 +1573430400000;1632 +1573516800000;1640 +1573603200000;1631 +1573689600000;1637 +1573776000000;1640 +1573862400000;1626 +1573948800000;1609 +1574035200000;1603 +1574121600000;1637 +1574208000000;1635 +1574294400000;1632 +1574380800000;1654 +1574467200000;1653 +1574553600000;1651 +1574640000000;1644 +1574726400000;1625 +1574812800000;1653 +1574899200000;1654 +1574985600000;1654 +1575072000000;1798 +1575158400000;1777 +1575244800000;1773 +1575331200000;1776 +1575417600000;1784 +1575504000000;1761 +1575590400000;1798 +1575676800000;1792 +1575763200000;1793 +1575849600000;1791 +1575936000000;1790 +1576022400000;1814 +1576108800000;1823 +1576195200000;1817 +1576281600000;1786 +1576368000000;1781 +1576454400000;1805 +1576540800000;1798 +1576627200000;1792 +1576713600000;1805 +1576800000000;1801 +1576886400000;1805 +1576972800000;1811 +1577059200000;1817 +1577145600000;1796 +1577232000000;1788 +1577318400000;1821 +1577404800000;1826 +1577491200000;1824 +1577577600000;1820 +1577664000000;1817 +1577750400000;1821 +1577836800000;1821 +1577923200000;1815 +1578009600000;1784 +1578096000000;1809 +1578182400000;1817 +1578268800000;1800 +1578355200000;1790 +1578441600000;1789 +1578528000000;1787 +1578614400000;1765 +1578700800000;1720 +1578787200000;1656 +1578873600000;1671 +1578960000000;1607 +1579046400000;1557 +1579132800000;1529 +1579219200000;1509 +1579305600000;1604 +1579392000000;1603 +1579478400000;1608 +1579564800000;1642 +1579651200000;1641 +1579737600000;1652 +1579824000000;1653 +1579910400000;1643 +1579996800000;1654 +1580083200000;1659 +1580169600000;1669 +1580256000000;1689 +1580342400000;1684 +1580428800000;1691 +1580515200000;1694 +1580601600000;1676 +1580688000000;1643 +1580774400000;1610 +1580860800000;1585 +1580947200000;1554 +1581033600000;1526 +1581120000000;1522 +1581206400000;1517 +1581292800000;1509 +1581379200000;1512 +1581465600000;1512 +1581552000000;1519 +1581638400000;1544 +1581724800000;1546 +1581811200000;1549 +1581897600000;1550 +1581984000000;1546 +1582070400000;1564 +1582156800000;1578 +1582243200000;1578 +1582329600000;1578 +1582416000000;1606 +1582502400000;1607 +1582588800000;1618 +1582675200000;1617 +1582761600000;1622 +1582848000000;1623 +1582934400000;1624 +1583020800000;1622 +1583107200000;1620 +1583193600000;1609 +1583280000000;1617 +1583366400000;1606 +1583452800000;1610 +1583539200000;1608 +1583625600000;1624 +1583712000000;1617 +1583798400000;1620 +1583884800000;1623 +1583971200000;1634 +1584057600000;1629 +1584144000000;1629 +1584230400000;1634 +1584316800000;1627 +1584403200000;1616 +1584489600000;1630 +1584576000000;1631 +1584662400000;1624 +1584748800000;1620 +1584835200000;1623 +1584921600000;1620 +1585008000000;1615 +1585094400000;1613 +1585180800000;1605 +1585267200000;1616 +1585353600000;1604 +1585440000000;1601 +1585522800000;1599 +1585609200000;1607 +1585695600000;1597 +1585782000000;1604 +1585868400000;1602 +1585954800000;1597 +1586041200000;1594 +1586127600000;1611 +1586214000000;1613 +1586300400000;1612 +1586386800000;1614 +1586473200000;1614 +1586559600000;1618 +1586646000000;1613 +1586732400000;1613 +1586818800000;1613 +1586905200000;1606 +1586991600000;1621 +1587078000000;1623 +1587164400000;1625 +1587250800000;1617 +1587337200000;1615 +1587423600000;1615 +1587510000000;1614 +1587596400000;1616 +1587682800000;1628 +1587769200000;1621 +1587855600000;1588 +1587942000000;1559 +1588028400000;1530 +1588114800000;1520 +1588201200000;1518 +1588287600000;1514 +1588374000000;1514 +1588460400000;1509 +1588546800000;1514 +1588633200000;1534 +1588719600000;1529 +1588806000000;1567 +1588892400000;1568 +1588978800000;1569 +1589065200000;1568 +1589151600000;1570 +1589238000000;1604 +1589324400000;1597 +1589410800000;1589 +1589497200000;1563 +1589583600000;1557 +1589670000000;1553 +1589756400000;1533 +1589842800000;1537 +1589929200000;1532 +1590015600000;1525 +1590102000000;1519 +1590188400000;1517 +1590274800000;1514 +1590361200000;1544 +1590447600000;1549 +1590534000000;1540 +1590620400000;1538 +1590706800000;1549 +1590793200000;1558 +1590879600000;1559 +1590966000000;1514 +1591052400000;1516 +1591138800000;1523 +1591225200000;1526 +1591311600000;1529 +1591398000000;1558 +1591484400000;1563 +1591570800000;1570 +1591657200000;1571 +1591743600000;1577 +1591830000000;1571 +1591916400000;1570 +1592002800000;1579 +1592089200000;1572 +1592175600000;1572 +1592262000000;1587 +1592348400000;1599 +1592434800000;1608 +1592521200000;1607 +1592607600000;1612 +1592694000000;1627 +1592780400000;1632 +1592866800000;1633 +1592953200000;1629 +1593039600000;1630 +1593126000000;1645 +1593212400000;1648 +1593298800000;1650 +1593385200000;1653 +1593471600000;1654 +1593558000000;1657 +1593644400000;1663 +1593730800000;1666 +1593817200000;1661 +1593903600000;1668 +1593990000000;1666 +1594076400000;1667 +1594162800000;1664 +1594249200000;1671 +1594335600000;1660 +1594422000000;1664 +1594508400000;1710 +1594594800000;1717 +1594681200000;1722 +1594767600000;1729 +1594854000000;1729 +1594940400000;1725 +1595026800000;1723 +1595113200000;1719 +1595199600000;1700 +1595286000000;1627 +1595372400000;1620 +1595458800000;1616 +1595545200000;1609 +1595631600000;1674 +1595718000000;1735 +1595804400000;1746 +1595890800000;1739 +1595977200000;1748 +1596063600000;1754 +1596150000000;1751 +1596236400000;1738 +1596322800000;1736 +1596409200000;1725 +1596495600000;1734 +1596582000000;1732 +1596668400000;1738 +1596754800000;1759 +1596841200000;1765 +1596927600000;1767 +1597014000000;1759 +1597100400000;1760 +1597186800000;1762 +1597273200000;1765 +1597359600000;1760 +1597446000000;1766 +1597532400000;1756 +1597618800000;1744 +1597705200000;1736 +1597791600000;1741 +1597878000000;1754 +1597964400000;1755 +1598050800000;1758 +1598137200000;1966 +1598223600000;1968 +1598310000000;1975 +1598396400000;1975 +1598482800000;1982 +1598569200000;1980 +1598655600000;1994 +1598742000000;1977 +1598828400000;1855 +1598914800000;1820 +1599001200000;2003 +1599087600000;2006 +1599174000000;2004 +1599260400000;1987 +1599346800000;1985 +1599433200000;1993 +1599519600000;1994 +1599606000000;1997 +1599692400000;1998 +1599778800000;1988 +1599865200000;1984 +1599951600000;1983 +1600038000000;1987 +1600124400000;1994 +1600210800000;1991 +1600297200000;2000 +1600383600000;2007 +1600470000000;2009 +1600556400000;2010 +1600642800000;2012 +1600729200000;2023 +1600815600000;2026 +1600902000000;2024 +1600988400000;2024 +1601074800000;2018 +1601161200000;2022 +1601247600000;2024 +1601334000000;2030 +1601420400000;2036 +1601506800000;2033 +1601593200000;2037 +1601679600000;2028 +1601766000000;2028 +1601852400000;2029 +1601938800000;2029 +1602025200000;2026 +1602111600000;2028 +1602198000000;2027 +1602284400000;2031 +1602370800000;2041 +1602457200000;2040 +1602543600000;2042 +1602630000000;2042 +1602716400000;2042 +1602802800000;2042 +1602889200000;2041 +1602975600000;2041 +1603062000000;2037 +1603148400000;2043 +1603234800000;2039 +1603321200000;2037 +1603407600000;2047 +1603494000000;2048 +1603580400000;2050 +1603670400000;2039 +1603756800000;2042 +1603843200000;2039 +1603929600000;2034 +1604016000000;2030 +1604102400000;2031 +1604188800000;2031 +1604275200000;2027 +1604361600000;2030 +1604448000000;2034 +1604534400000;2030 +1604620800000;1969 +1604707200000;1904 +1604793600000;1905 +1604880000000;1903 +1604966400000;1901 +1605052800000;1905 +1605139200000;1887 +1605225600000;1865 +1605312000000;1865 +1605398400000;1883 +1605484800000;1895 +1605571200000;1905 +1605657600000;1902 +1605744000000;1919 +1605830400000;1920 +1605916800000;1924 +1606003200000;1918 +1606089600000;1911 +1606176000000;1904 +1606262400000;1907 +1606348800000;1906 +1606435200000;1898 +1606521600000;1904 +1606608000000;1909 +1606694400000;1912 +1606780800000;1911 +1606867200000;1899 +1606953600000;1897 +1607040000000;1897 +1607126400000;1903 +1607212800000;1923 +1607299200000;1922 +1607385600000;1891 +1607472000000;1805 +1607558400000;1742 +1607644800000;1703 +1607731200000;1684 +1607817600000;1678 +1607904000000;1672 +1607990400000;1678 +1608076800000;1691 +1608163200000;1697 +1608249600000;1708 +1608336000000;1708 +1608422400000;1711 +1608508800000;1743 +1608595200000;1745 +1608681600000;1750 +1608768000000;1748 +1608854400000;1751 +1608940800000;1754 +1609027200000;1755 +1609113600000;1756 +1609200000000;1741 +1609286400000;1746 +1609372800000;1772 +1609459200000;1774 +1609545600000;1767 +1609632000000;1778 +1609718400000;1785 +1609804800000;1801 +1609891200000;1805 +1609977600000;1807 +1610064000000;1805 +1610150400000;1955 +1610236800000;1948 +1610323200000;1942 +1610409600000;1931 +1610496000000;1939 +1610582400000;1956 +1610668800000;1954 +1610755200000;1959 +1610841600000;1962 +1610928000000;1959 +1611014400000;1959 +1611100800000;1963 +1611187200000;1963 +1611273600000;1964 +1611360000000;1974 +1611446400000;1970 +1611532800000;1963 +1611619200000;1958 +1611705600000;1960 +1611792000000;1960 +1611878400000;1960 +1611964800000;1956 +1612051200000;1956 +1612137600000;1965 +1612224000000;1968 +1612310400000;1970 +1612396800000;1973 +1612483200000;1979 +1612569600000;1985 +1612656000000;1966 +1612742400000;1974 +1612828800000;1975 +1612915200000;1972 +1613001600000;1979 +1613088000000;1974 +1613174400000;1973 +1613260800000;1972 +1613347200000;2023 +1613433600000;2258 +1613520000000;2258 +1613606400000;2261 +1613692800000;2270 +1613779200000;2274 +1613865600000;2395 +1613952000000;2539 +1614038400000;2641 +1614124800000;2645 +1614211200000;2647 +1614297600000;2644 +1614384000000;2644 +1614470400000;2643 +1614556800000;2641 +1614643200000;2638 +1614729600000;2639 +1614816000000;2644 +1614902400000;2645 +1614988800000;2644 +1615075200000;2653 +1615161600000;2650 +1615248000000;2656 +1615334400000;2658 +1615420800000;2665 +1615507200000;2669 +1615593600000;2659 +1615680000000;2557 +1615766400000;2554 +1615852800000;2547 +1615939200000;2547 +1616025600000;2542 +1616112000000;2544 +1616198400000;2541 +1616284800000;2535 +1616371200000;2538 +1616457600000;2539 +1616544000000;2537 +1616630400000;2538 +1616716800000;2542 +1616803200000;2541 +1616889600000;2531 +1616972400000;2543 +1617058800000;2542 +1617145200000;2546 +1617231600000;2545 +1617318000000;2540 +1617404400000;2545 +1617490800000;2542 +1617577200000;2544 +1617663600000;2549 +1617750000000;2554 +1617836400000;2551 +1617922800000;2538 +1618009200000;2534 +1618095600000;2535 +1618182000000;2534 +1618268400000;2531 +1618354800000;2538 +1618441200000;2538 +1618527600000;2538 +1618614000000;2538 +1618700400000;2526 +1618786800000;2541 +1618873200000;2509 +1618959600000;2486 +1619046000000;2463 +1619132400000;2467 +1619218800000;2469 +1619305200000;2471 +1619391600000;2469 +1619823600000;677 +1619910000000;1066 +1619996400000;1615 +1620082800000;1668 +1620169200000;1804 +1620255600000;1844 +1620342000000;1967 +1620428400000;2189 +1620514800000;2191 +1620601200000;2194 +1620687600000;2203 +1620774000000;2212 +1620860400000;2207 +1620946800000;2203 +1621033200000;2198 +1621119600000;2203 +1621206000000;2244 +1621292400000;2309 +1621378800000;2355 +1621465200000;2360 +1621551600000;2355 +1621638000000;2346 +1621724400000;2353 +1621810800000;2355 +1621897200000;2351 +1621983600000;2368 +1622070000000;2364 +1622156400000;2362 +1622242800000;2358 +1622329200000;2349 +1622415600000;2348 +1622502000000;2375 +1622588400000;2373 +1622674800000;2373 +1622761200000;2369 +1622847600000;2368 +1622934000000;2365 +1623020400000;2369 +1623106800000;2368 +1623193200000;2370 +1623279600000;2370 +1623366000000;2368 +1623452400000;2365 +1623538800000;2342 +1623625200000;2305 +1623711600000;2335 +1623798000000;2338 +1623884400000;2331 +1623970800000;2321 +1624057200000;2311 +1624143600000;2306 +1624230000000;2300 +1624316400000;2316 +1624402800000;2313 +1624489200000;2324 +1624575600000;2330 +1624662000000;2330 +1624748400000;2334 +1624834800000;2335 +1624921200000;2341 +1625007600000;2340 +1625094000000;2341 +1625180400000;2342 +1625266800000;2339 +1625353200000;2383 +1625439600000;2388 +1625526000000;2389 +1625612400000;2389 +1625698800000;2388 +1625785200000;2388 +1625871600000;2388 +1625958000000;2388 +1626044400000;2388 +1626130800000;2397 +1626217200000;2399 +1626303600000;2399 +1626390000000;2398 +1626476400000;2402 +1626562800000;2403 +1626649200000;2402 +1626735600000;2403 +1626822000000;2402 +1626908400000;2403 +1626994800000;2403 +1627081200000;2428 +1627167600000;2439 +1627254000000;2440 +1627340400000;2455 +1627426800000;2456 +1627513200000;2462 +1627599600000;2463 +1627686000000;2468 +1627772400000;2468 +1627858800000;2467 +1627945200000;2465 +1628031600000;2465 +1628118000000;2463 +1628204400000;2465 +1628290800000;2463 +1628377200000;2466 +1628463600000;2470 +1628550000000;2469 +1628636400000;2469 +1628722800000;2472 +1628809200000;2474 +1628895600000;2476 +1628982000000;2478 +1629068400000;2478 +1629154800000;2480 +1629241200000;2480 +1629327600000;2481 +1629414000000;2511 +1629500400000;2510 +1629586800000;2512 +1629673200000;2511 +1629759600000;2514 +1629846000000;2514 +1629932400000;2515 +1630018800000;2515 +1630105200000;2511 +1630191600000;2506 +1630278000000;2505 +1630364400000;2506 +1630450800000;2503 +1630537200000;2506 +1630623600000;2518 +1630710000000;2517 +1630796400000;2513 +1630882800000;2507 +1630969200000;2505 +1631055600000;2508 +1631142000000;2511 +1631228400000;2513 +1631314800000;2512 +1631401200000;2512 +1631487600000;2510 +1631574000000;2511 +1631660400000;2511 +1631746800000;2511 +1631833200000;2511 +1631919600000;2509 +1632006000000;2512 +1632092400000;2501 +1632178800000;2502 +1632265200000;2503 +1632351600000;2502 +1632438000000;2503 +1632524400000;2501 +1632610800000;2501 +1632697200000;2502 +1632783600000;2501 +1632870000000;2502 +1632956400000;2502 +1633042800000;2502 +1633129200000;2503 +1633215600000;2503 +1633302000000;2504 +1633388400000;2505 +1633474800000;2502 +1633561200000;2502 +1633647600000;2503 +1633734000000;2505 +1633820400000;2501 +1633906800000;2500 +1633993200000;2501 +1634079600000;2506 +1634166000000;2506 +1634252400000;2506 +1634338800000;2505 +1634425200000;2436 +1634511600000;2437 +1634598000000;2451 +1634684400000;2450 +1634770800000;2448 +1634857200000;2449 +1634943600000;2450 +1635030000000;2450 +1635116400000;2456 +1635202800000;2489 +1635289200000;2489 +1635375600000;2489 +1635462000000;2483 +1635548400000;2480 +1635634800000;2478 +1635724800000;2482 +1635811200000;2481 +1635897600000;2479 +1635984000000;2485 +1636070400000;2472 +1636156800000;2471 +1636243200000;2470 +1636329600000;2467 +1636416000000;2468 +1636502400000;2468 +1636588800000;2467 +1636675200000;2467 +1636761600000;2467 +1636848000000;2468 +1636934400000;2465 +1637020800000;2461 +1637107200000;2468 +1637193600000;2469 +1637280000000;2468 +1637366400000;2469 +1637452800000;2469 +1637539200000;2469 +1637625600000;2468 +1637712000000;2469 +1637798400000;2470 +1637884800000;2471 +1637971200000;2471 +1638057600000;2467 +1638144000000;2463 +1638230400000;2463 +1638316800000;2446 +1638403200000;2439 +1638489600000;2438 +1638576000000;2438 +1638662400000;2439 +1638748800000;2437 +1638835200000;2434 +1638921600000;2413 +1639008000000;2398 +1639094400000;2400 +1639180800000;2391 +1639267200000;2386 +1639353600000;2386 +1639440000000;2381 +1639526400000;2367 +1639612800000;2366 +1639699200000;2367 +1639785600000;2367 +1639872000000;2371 +1639958400000;2372 +1640044800000;2385 +1640131200000;2384 +1640217600000;2384 +1640304000000;2376 +1640390400000;2379 +1640476800000;2379 +1640563200000;2391 +1640649600000;2392 +1640736000000;2420 +1640822400000;2421 +1640908800000;2421 +1640995200000;2420 +1641081600000;2414 +1641168000000;2420 +1641254400000;2421 +1641340800000;2423 +1641427200000;2424 +1641513600000;2430 +1641600000000;2430 +1641686400000;2430 +1641772800000;2431 +1641859200000;2432 +1641945600000;2432 +1642032000000;2434 +1642118400000;2436 +1642204800000;2437 +1642291200000;2435 +1642377600000;2435 +1642464000000;2436 +1642550400000;2433 +1642636800000;2434 +1642723200000;2433 +1642809600000;2418 +1642896000000;2420 +1642982400000;2414 +1643068800000;2412 +1643155200000;2412 +1643241600000;2421 +1643328000000;2422 +1643414400000;2422 +1643500800000;2417 +1643587200000;2411 +1643673600000;2412 +1643760000000;2412 +1643846400000;2410 +1643932800000;2412 +1644019200000;2411 +1644105600000;2412 +1644192000000;2412 +1644278400000;2413 +1644364800000;2413 +1644451200000;2414 +1644537600000;2413 +1644624000000;2411 +1644710400000;2415 +1644796800000;2417 +1644883200000;2416 +1644969600000;2413 +1645056000000;2414 +1645142400000;2411 +1645228800000;2405 +1645315200000;2407 +1645401600000;2409 +1645488000000;2406 +1645574400000;2406 +1645660800000;2404 +1645747200000;2406 +1645833600000;2405 +1645920000000;2403 +1646006400000;2403 +1646092800000;2404 +1646179200000;2403 +1646265600000;2402 +1646352000000;2392 +1646438400000;2382 +1646524800000;2380 +1646611200000;2376 +1646697600000;2375 +1646784000000;2373 +1646870400000;2368 +1646956800000;2372 +1647043200000;2372 +1647129600000;2375 +1647216000000;2378 +1647302400000;2372 +1647388800000;2373 +1647475200000;2391 +1647561600000;2394 +1647648000000;2393 +1647734400000;2396 +1647820800000;2396 +1647907200000;2395 +1647993600000;2398 +1648080000000;2393 +1648166400000;2390 +1648252800000;2379 +1648339200000;2399 +1648422000000;2399 +1648508400000;2396 +1648594800000;2397 +1648681200000;2402 +1648767600000;2401 +1648854000000;2399 +1648940400000;2409 +1649026800000;2409 +1649113200000;2409 +1649199600000;2408 +1649286000000;2408 +1649372400000;2410 +1649458800000;2410 +1649545200000;2410 +1649631600000;2412 +1649718000000;2412 +1649804400000;2412 +1649890800000;2412 +1649977200000;2412 +1650063600000;2410 +1650150000000;2411 +1650236400000;2409 +1650322800000;2410 +1650409200000;2410 +1650495600000;2410 +1650582000000;2408 +1650841200000;2405 +1650927600000;2407 +1651014000000;2406 +1651100400000;2403 +1651186800000;2401 +1651273200000;2395 +1651359600000;2392 +1651446000000;2388 +1651532400000;2388 +1651618800000;2389 +1651705200000;2388 +1651791600000;2386 +1651878000000;2387 +1651964400000;2387 +1652050800000;2384 +1652137200000;2384 +1652223600000;2382 +1652310000000;2391 +1652396400000;2397 +1652482800000;2397 +1652569200000;2399 +1652655600000;2403 +1652742000000;2399 +1652828400000;2399 +1652914800000;2399 +1653001200000;2401 +1653087600000;2401 +1653174000000;2405 +1653260400000;2410 +1653346800000;2417 +1653433200000;2417 +1653519600000;2415 +1653606000000;2417 +1653692400000;2414 +1653778800000;2412 +1653865200000;2417 +1653951600000;2415 +1654038000000;2412 +1654124400000;2426 +1654210800000;2426 +1654297200000;2425 +1654383600000;2427 +1654470000000;2430 +1654556400000;2434 +1654642800000;2434 +1654729200000;2434 +1654815600000;2436 +1654902000000;2437 +1654988400000;2436 +1655074800000;2436 +1655161200000;2437 +1655247600000;2437 +1655334000000;2436 +1655420400000;2434 +1655506800000;2435 +1655593200000;2438 +1655679600000;2436 +1655766000000;2433 +1655852400000;2435 +1655938800000;2435 +1656025200000;2433 +1656111600000;2435 +1656198000000;2441 +1656284400000;2446 +1656370800000;2447 +1656457200000;2446 +1656543600000;2446 +1656630000000;2445 +1656716400000;2448 +1656802800000;2448 +1656889200000;2449 +1656975600000;2449 +1657062000000;2449 +1657148400000;2450 +1657234800000;2449 +1657321200000;2453 +1657407600000;2451 +1657494000000;2456 +1657580400000;2456 +1657666800000;2456 +1657753200000;2462 +1657839600000;2462 +1657926000000;2468 +1658012400000;2469 +1658098800000;2471 +1658185200000;2470 +1658271600000;2471 +1658358000000;2471 +1658444400000;2469 +1658530800000;2471 +1658617200000;2471 +1658703600000;2471 +1658790000000;2470 +1658876400000;2470 +1658962800000;2470 +1659049200000;2469 +1659135600000;2470 +1659222000000;2474 +1659308400000;2473 +1659394800000;2473 +1659481200000;2473 +1659654000000;2459 +1659740400000;2445 +1659826800000;2421 +1659913200000;2407 +1659999600000;2396 +1660086000000;2403 +1660172400000;2399 +1660258800000;2398 +1660345200000;2411 +1660431600000;2425 +1660518000000;2422 +1660604400000;2422 +1660690800000;2415 +1660777200000;2414 +1660863600000;2414 +1660950000000;2409 +1661036400000;2412 +1661122800000;2408 +1661209200000;2407 +1661295600000;2404 +1661382000000;2397 +1661468400000;2395 +1661554800000;2393 +1661641200000;2391 +1661727600000;2389 +1661814000000;2382 +1661900400000;2381 +1661986800000;2381 +1662073200000;2346 +1662159600000;2347 +1662246000000;2355 +1662332400000;2354 +1662418800000;2359 +1662505200000;2358 +1662591600000;2356 +1662678000000;2356 +1662764400000;2356 +1662850800000;2361 +1662937200000;2388 +1663023600000;2391 +1663110000000;2385 +1663196400000;2360 +1663282800000;2360 +1663369200000;2360 +1663455600000;2363 +1663542000000;2365 +1663628400000;2362 +1663714800000;2364 +1663801200000;2363 +1663887600000;2363 +1663974000000;2366 +1664060400000;2366 +1664146800000;2367 +1664233200000;2367 +1664319600000;2365 +1664406000000;2365 +1664492400000;2370 +1664578800000;2367 +1664665200000;2367 +1664751600000;2370 +1664838000000;2371 +1664924400000;2372 +1665010800000;2372 +1665097200000;2375 +1665183600000;2375 +1665270000000;2373 +1665356400000;2375 +1665442800000;2374 +1665529200000;2374 +1665615600000;2375 +1665702000000;2375 +1665788400000;2373 +1665874800000;2373 +1665961200000;2370 +1666047600000;2372 +1666134000000;2370 +1666220400000;2371 +1666306800000;2372 +1666393200000;2373 +1666479600000;2370 +1666566000000;2373 +1666652400000;2378 +1666738800000;2378 +1666825200000;2375 +1666911600000;2376 +1666998000000;2371 +1667084400000;2367 +1667174400000;2370 +1667260800000;2366 +1667347200000;2360 +1667433600000;2358 +1667520000000;2357 +1667606400000;2358 +1667692800000;2361 +1667779200000;2360 +1667865600000;2359 +1667952000000;2355 +1668038400000;2355 +1668124800000;2351 +1668211200000;2343 +1668297600000;2347 +1668384000000;2354 +1668470400000;2353 +1668556800000;2356 +1668643200000;2362 +1668729600000;2361 +1668816000000;2361 +1668902400000;2362 +1668988800000;2369 +1669075200000;2367 +1669161600000;2365 +1669248000000;2366 +1669334400000;2366 +1669420800000;2363 +1669507200000;2358 +1669593600000;2364 +1669680000000;2363 +1669766400000;2365 +1669852800000;2367 +1669939200000;2367 +1670025600000;2379 +1670112000000;2378 +1670198400000;2375 +1670284800000;2374 +1670371200000;2376 +1670457600000;2375 +1670544000000;2372 +1670630400000;2371 +1670716800000;2371 +1670803200000;2371 +1670889600000;2367 +1670976000000;2368 +1671062400000;2371 +1671148800000;2382 +1671235200000;2382 +1671321600000;2379 +1671408000000;2381 +1671494400000;2378 +1671580800000;2376 +1671667200000;2374 +1671753600000;2375 +1671840000000;2373 +1671926400000;2372 +1672012800000;2369 +1672099200000;2369 +1672185600000;2369 +1672272000000;2368 +1672358400000;2369 +1672444800000;2369 +1672531200000;2368 +1672617600000;2374 +1672704000000;2379 +1672790400000;2412 +1672876800000;2415 +1672963200000;2410 +1673049600000;2411 +1673136000000;2418 +1673222400000;2419 +1673308800000;2428 +1673395200000;2429 +1673481600000;2429 +1673568000000;2422 +1673654400000;2423 +1673740800000;2419 +1673827200000;2412 +1673913600000;2409 +1674000000000;2412 +1674086400000;2412 +1674172800000;2407 +1674259200000;2420 +1674345600000;2417 +1674432000000;2419 +1674518400000;2420 +1674604800000;2418 +1674691200000;2414 +1674777600000;2428 +1674864000000;2432 +1674950400000;2428 +1675036800000;2436 +1675123200000;2436 +1675209600000;2436 +1675296000000;2433 +1675382400000;2434 +1675468800000;2428 +1675555200000;2434 +1675641600000;2432 +1675728000000;2433 +1675814400000;2432 +1675900800000;2428 +1675987200000;2424 +1676073600000;2417 +1676160000000;2419 +1676246400000;2419 +1676332800000;2420 +1676419200000;2416 +1676505600000;2418 +1676592000000;2419 +1676678400000;2421 +1676764800000;2419 +1676851200000;2416 +1676937600000;2414 +1677024000000;2414 +1677110400000;2422 +1677196800000;2419 +1677283200000;2416 +1677369600000;2415 +1677456000000;2412 +1677542400000;2410 +1677628800000;2412 +1677715200000;2411 +1677801600000;2410 +1677888000000;2405 +1677974400000;2401 +1678060800000;2397 +1678147200000;2395 +1678233600000;2394 +1678320000000;2395 +1678406400000;2393 +1678492800000;2392 +1678579200000;2393 +1678665600000;2394 +1678752000000;2399 +1678838400000;2402 +1678924800000;2400 +1679011200000;2399 +1679097600000;2409 +1679184000000;2406 +1679270400000;2406 +1679356800000;2404 +1679443200000;2408 +1679529600000;2407 +1679616000000;2407 +1680390000000;2421 +1680476400000;2421 +1680562800000;2423 +1680649200000;2421 +1680735600000;2423 +1680822000000;2423 +1680908400000;2426 +1680994800000;2429 +1681081200000;2429 +1681167600000;2429 +1681254000000;2430 +1681340400000;2431 +1681426800000;2435 +1681513200000;2439 +1681599600000;2444 +1681686000000;2444 +1681772400000;2444 +1681858800000;2447 +1681945200000;2446 +1682031600000;2447 +1682118000000;2447 +1682204400000;2446 +1682290800000;2444 +1682377200000;2446 +1682463600000;2446 +1682550000000;2444 +1682636400000;2443 +1682722800000;2443 +1682809200000;2445 +1682895600000;2445 +1682982000000;2452 +1683068400000;2452 +1683154800000;2454 +1683241200000;2452 +1683327600000;2455 +1683414000000;2455 +1683500400000;2456 +1683586800000;2458 +1683673200000;2458 +1683759600000;2459 +1683846000000;2459 +1683932400000;2458 +1684018800000;2457 +1684105200000;2458 +1684191600000;2459 +1684278000000;2461 +1684364400000;2464 +1684450800000;2463 +1684537200000;2463 +1684623600000;2464 +1684710000000;2465 +1684796400000;2467 +1684882800000;2465 +1684969200000;2466 +1685055600000;2465 +1685142000000;2467 +1685228400000;2468 +1685314800000;2470 +1685401200000;2468 +1685487600000;2471 +1685574000000;2506 +1685660400000;2505 +1685746800000;2505 +1685833200000;2505 +1685919600000;2505 +1686006000000;2507 +1686092400000;2508 +1686178800000;2509 +1686265200000;2507 +1686351600000;2508 +1686438000000;2512 +1686524400000;2517 +1686610800000;2518 +1686697200000;2518 +1686783600000;2517 +1686870000000;2526 +1686956400000;2528 +1687042800000;2526 +1687129200000;2527 +1687215600000;2527 +1687302000000;2527 +1687388400000;2528 +1687474800000;2529 +1687561200000;2528 +1687647600000;2528 +1687734000000;2532 +1687820400000;2533 +1687906800000;2535 +1687993200000;2535 +1688079600000;2533 +1688166000000;2532 +1688252400000;2532 +1688338800000;2533 +1688425200000;2534 +1688511600000;2537 +1688598000000;2538 +1688684400000;2536 +1688770800000;2534 +1688857200000;2538 +1688943600000;2542 +1689030000000;2543 +1689116400000;2542 +1689202800000;2542 +1689289200000;2549 +1689375600000;2550 +1689462000000;2554 +1689548400000;2557 +1689552000000;2681 +1689638400000;2682 +1689724800000;2686 +1689811200000;2697 +1689897600000;2697 +1689984000000;2699 +1690070400000;2716 +1690156800000;2716 +1690243200000;2715 +1690329600000;2717 +1690416000000;2718 +1690502400000;2718 +1690588800000;2718 +1690675200000;2718 +1690761600000;2718 +1690848000000;2718 +1690934400000;2718 +1691020800000;2718 +1691107200000;2719 +1691193600000;2721 +1691280000000;2721 +1691366400000;2722 +1691452800000;2723 +1691539200000;2729 +1691625600000;2729 +1691712000000;2730 +1691798400000;2730 +1691884800000;2730 +1691971200000;2730 +1692057600000;2729 +1692144000000;2729 +1692230400000;2729 +1692316800000;2726 +1692403200000;2726 +1692489600000;2726 +1692576000000;2726 +1692662400000;2725 +1692748800000;2725 +1692835200000;2724 +1692921600000;2724 +1693008000000;2724 +1693094400000;2724 +1693180800000;2724 +1693267200000;2733 +1693353600000;2734 +1693440000000;2735 +1693526400000;2741 +1693612800000;2741 +1693699200000;2744 +1693785600000;2743 +1693872000000;2742 +1693958400000;2752 +1694044800000;2751 +1694131200000;2751 +1694217600000;2751 +1694304000000;2751 +1694390400000;2751 +1694476800000;2751 +1694563200000;2753 +1694649600000;2756 +1694736000000;2756 +1694822400000;2756 +1694908800000;2758 +1694995200000;2758 +1695081600000;2757 +1695168000000;2755 +1695254400000;2754 +1695340800000;2754 +1695427200000;2753 +1695513600000;2754 +1695600000000;2754 +1695686400000;2754 +1695772800000;2753 +1695859200000;2758 +1695945600000;2759 +1696032000000;2759 +1696118400000;2759 +1696204800000;2760 +1696291200000;2760 +1696377600000;2760 +1696464000000;2761 +1696550400000;2761 +1696636800000;2761 +1696723200000;2761 +1696809600000;2761 +1696896000000;2763 +1696982400000;2762 +1697068800000;2761 +1697155200000;2761 +1697241600000;2761 +1697328000000;2761 +1697414400000;2761 +1697500800000;2761 +1697587200000;2762 +1697673600000;2762 +1697760000000;2762 +1697846400000;2762 +1697932800000;2762 +1698019200000;2762 +1698105600000;2761 +1698192000000;2761 +1698278400000;2761 +1698364800000;2761 +1698451200000;2761 +1698537600000;2761 +1698624000000;2761 +1698710400000;2761 +1698796800000;2770 +1698883200000;2770 +1698969600000;2770 +1699056000000;2770 +1699142400000;2769 +1699228800000;2768 +1699315200000;2766 +1699401600000;2767 +1699488000000;2767 +1699574400000;2767 +1699660800000;2767 +1699747200000;2767 +1699833600000;2767 +1699920000000;2768 +1700006400000;2768 +1700092800000;2767 +1700179200000;2764 +1700265600000;2764 +1700352000000;2764 +1700438400000;2764 +1700524800000;2764 +1700611200000;2771 +1700697600000;2772 +1700784000000;2772 +1700870400000;2774 +1700956800000;2774 +1701043200000;2776 +1701129600000;2774 +1701216000000;2771 +1701302400000;2772 +1701388800000;2773 +1701475200000;2773 +1701561600000;2776 +1701648000000;2776 +1701734400000;2773 +1701820800000;2773 +1701907200000;2773 +1701993600000;2776 +1702080000000;2776 +1702166400000;2777 +1702252800000;2777 +1702339200000;2776 +1702425600000;2775 +1702512000000;2775 +1702598400000;2775 +1702684800000;2774 +1702771200000;2776 +1702857600000;2776 +1702944000000;2776 +1703030400000;2776 +1703116800000;2773 +1703203200000;2773 +1703289600000;2772 +1703376000000;2770 +1703462400000;2769 +1703548800000;2770 +1703635200000;2770 +1703721600000;2770 +1703808000000;2770 +1703894400000;2768 +1703980800000;2768 +1704067200000;2767 +1704153600000;2767 +1704240000000;2767 +1704326400000;2767 +1704412800000;2767 +1704499200000;2766 +1704585600000;2766 +1704672000000;2766 +1704758400000;2767 +1704844800000;2769 +1704931200000;2769 +1705017600000;2771 +1705104000000;2771 +1705190400000;2771 +1705276800000;2772 +1705363200000;2767 +1705449600000;2766 +1705536000000;2766 +1705622400000;2766 +1705708800000;2766 +1705795200000;2766 +1705881600000;2767 +1705968000000;2767 +1706054400000;2768 +1706140800000;2768 +1706227200000;2763 +1706313600000;2763 +1706400000000;2764 +1706486400000;2764 +1706572800000;2762 +1706659200000;2762 +1706745600000;2761 +1706832000000;2761 +1706918400000;2762 +1707004800000;2761 +1707091200000;2762 +1707177600000;2764 +1707264000000;2767 +1707350400000;2767 +1707436800000;2767 +1707523200000;2767 +1707609600000;2769 +1707696000000;2769 +1707782400000;2767 +1707868800000;2766 +1707955200000;2766 +1708041600000;2771 +1708128000000;2771 +1708214400000;2767 +1708300800000;2767 +1708387200000;2766 +1708473600000;2764 +1708560000000;2764 +1708646400000;2760 +1708732800000;2760 +1708819200000;2760 +1708905600000;2760 +1708992000000;2760 +1709078400000;2760 +1709164800000;2755 +1709251200000;2753 +1709337600000;2750 +1709424000000;2748 +1709510400000;2755 +1709596800000;2752 +1709683200000;2752 +1709769600000;2758 +1709856000000;2758 +1709942400000;2758 +1710028800000;2755 +1710115200000;2753 +1710201600000;2744 +1710288000000;2741 +1710374400000;2740 +1710460800000;2736 +1710547200000;2736 +1710633600000;2736 +1710720000000;2736 +1710806400000;2736 +1710892800000;2734 +1710979200000;2735 +1711065600000;2736 +1711152000000;2743 +1711238400000;2742 +1711324800000;2743 +1711411200000;2743 +1711497600000;2742 +1711584000000;2744 +1711670400000;2744 +1711756800000;2744 +1711843200000;2746 +1711929600000;2746 +1712016000000;2746 +1712102400000;2745 +1712188800000;2744 +1712275200000;2745 +1712361600000;2743 +1712448000000;2743 +1712534400000;2741 +1712620800000;2741 +1712707200000;2739 +1712793600000;2739 +1712880000000;2739 +1712966400000;2739 +1713052800000;2739 +1713139200000;2738 +1713225600000;2738 +1713312000000;2738 +1713398400000;2738 +1713484800000;2739 +1713571200000;2739 +1713657600000;2739 +1713744000000;2740 +1713830400000;2740 +1713916800000;2740 +1714003200000;2740 +1714089600000;2740 +1714176000000;2740 +1714262400000;2740 +1714348800000;2740 +1714435200000;2740 +1714521600000;2739 +1714608000000;2737 +1714694400000;2737 +1714780800000;2737 +1714867200000;2737 +1714953600000;2737 +1715040000000;2737 +1715126400000;2737 +1715212800000;2737 +1715299200000;2736 +1715385600000;2736 +1715472000000;2736 +1715558400000;2736 +1715644800000;2736 +1715731200000;2735 +1715817600000;2735 +1715904000000;2734 +1715990400000;2733 +1716076800000;2733 +1716163200000;2733 +1716249600000;2737 +1716336000000;2736 +1716422400000;2736 +1716508800000;2736 +1716595200000;2735 +1716681600000;2734 +1716768000000;2734 +1716854400000;2734 +1716940800000;2734 +1717027200000;2733 +1717113600000;2733 +1717200000000;2733 +1717286400000;2733 +1717372800000;2733 +1717459200000;2733 +1717545600000;2733 +1717632000000;2732 +1717718400000;2731 +1717804800000;2738 +1717891200000;2738 +1717977600000;2738 +1718064000000;2739 +1718150400000;2738 +1718236800000;2738 +1718323200000;2738 +1718409600000;2738 +1718496000000;2738 +1718582400000;2739 +1718668800000;2739 +1718755200000;2740 +1718841600000;2741 +1718928000000;2741 +1719014400000;2741 +1719100800000;2741 +1719187200000;2741 +1719273600000;2739 +1719360000000;2739 +1719446400000;2740 +1719532800000;2741 +1719619200000;2742 +1719705600000;2742 +1719792000000;2742 +1719878400000;2745 +1719964800000;2747 +1720051200000;2734 +1720137600000;2734 +1720224000000;2722 +1720310400000;2724 +1720396800000;2725 +1720483200000;2723 +1720569600000;2724 +1720656000000;2724 +1720742400000;2724 +1720828800000;2723 +1720915200000;2723 +1721001600000;2726 +1721088000000;2726 +1721174400000;2727 +1721260800000;2726 +1721347200000;2723 +1721433600000;2722 +1721520000000;2722 +1721606400000;2722 +1721692800000;2721 +1721779200000;2720 +1721865600000;2720 +1721952000000;2718 +1722038400000;2718 +1722124800000;2718 +1722211200000;2718 +1722297600000;2718 +1722384000000;2717 +1722470400000;2713 +1722556800000;2711 +1722643200000;2710 +1722729600000;2711 +1722816000000;2711 +1722902400000;2696 +1722988800000;2697 +1723075200000;2697 +1723161600000;2699 +1723248000000;2700 +1723334400000;2701 +1723420800000;2701 +1723507200000;2702 +1723593600000;2702 +1723680000000;2703 +1723766400000;2703 +1723852800000;2703 +1723939200000;2703 +1724025600000;2694 +1724112000000;2687 +1724198400000;2688 +1724284800000;2688 +1724371200000;2690 +1724457600000;2684 +1724544000000;2684 +1724630400000;2684 +1724716800000;2684 +1724803200000;2684 +1724889600000;2683 +1724976000000;2683 +1725062400000;2683 +1725148800000;2683 +1725235200000;2683 +1725321600000;2685 +1725408000000;2685 +1725494400000;2685 +1725580800000;2685 +1725667200000;2685 +1725753600000;2685 +1725840000000;2685 +1725926400000;2685 +1726012800000;2685 +1726099200000;2685 +1726185600000;2685 +1726272000000;2685 +1726358400000;2685 +1726444800000;2686 +1726531200000;2686 +1726617600000;2686 +1726704000000;2686 +1726790400000;2686 +1726876800000;2689 +1726963200000;2689 +1727049600000;2689 +1727136000000;2689 +1727222400000;2690 +1727308800000;2690 +1727395200000;2685 +1727481600000;2684 +1727568000000;2684 +1727654400000;2686 +1727740800000;2698 +1727827200000;2698 +1727913600000;2698 +1728000000000;2698 +1728086400000;2698 +1728172800000;2698 +1728259200000;2698 +1728345600000;2699 +1728432000000;2698 +1728518400000;2698 +1728604800000;2698 +1728691200000;2699 +1728777600000;2699 +1728864000000;2693 +1728950400000;2693 +1729036800000;2693 +1729123200000;2693 +1729209600000;2693 +1729296000000;2693 +1729382400000;2693 +1729468800000;2693 +1729555200000;2693 +1729641600000;2693 +1729728000000;2693 +1729814400000;2692 +1729900800000;2692 +1729987200000;2692 +1730073600000;2692 +1730160000000;2692 +1730246400000;2692 +1730332800000;2693 +1730419200000;2693 +1730505600000;2692 +1730592000000;2693 +1730678400000;2692 +1730764800000;2691 +1730851200000;2691 +1730937600000;2691 +1731024000000;2691 +1731110400000;2691 +1731196800000;2692 +1731283200000;2693 +1731369600000;2694 +1731456000000;2694 +1731542400000;2693 +1731628800000;2693 +1731715200000;2690 +1731801600000;2690 +1731888000000;2690 +1731974400000;2689 +1732060800000;2689 +1732147200000;2687 +1732233600000;2684 +1732320000000;2685 +1732406400000;2688 +1732492800000;2688 +1732579200000;2685 +1732665600000;2685 +1732752000000;2685 +1732838400000;2685 +1732924800000;2685 +1733011200000;2684 +1733097600000;2685 +1733184000000;2685 +1733270400000;2683 +1733356800000;2679 +1733443200000;2678 +1733529600000;2678 +1733616000000;2678 +1733702400000;2688 +1733788800000;2685 +1733875200000;2685 +1733961600000;2685 +1734048000000;2683 +1734134400000;2684 +1734220800000;2684 +1734307200000;2707 +1734393600000;2706 +1734480000000;2706 +1734566400000;2706 +1734652800000;2707 +1734739200000;2708 +1734825600000;2708 +1734912000000;2708 +1734998400000;2708 +1735084800000;2709 +1735171200000;2710 +1735257600000;2711 +1735344000000;2708 +1735430400000;2707 +1735516800000;2707 +1735603200000;2707 +1735689600000;2707 +1735776000000;2708 +1735862400000;2706 +1735948800000;2706 +1736035200000;2706 +1736121600000;2706 +1736208000000;2706 +1736294400000;2706 +1736380800000;2706 +1736467200000;2706 +1736553600000;2706 +1736640000000;2706 +1736726400000;2706 +1736812800000;2706 +1736899200000;2706 +1736985600000;2706 +1737072000000;2706 +1737158400000;2707 +1737244800000;2710 +1737331200000;2707 +1737417600000;2705 +1737504000000;2705 +1737590400000;2704 +1737676800000;2702 +1737763200000;2702 +1737849600000;2703 +1737936000000;2702 +1738022400000;2699 +1738108800000;2705 +1738195200000;2704 +1738281600000;2703 +1738368000000;2704 +1738454400000;2704 +1738540800000;2702 +1738627200000;2704 +1738713600000;2704 +1738800000000;2704 +1738886400000;2704 +1738972800000;2710 +1739059200000;2710 +1739145600000;2712 +1739232000000;2713 +1739318400000;2713 +1739404800000;2711 +1739491200000;2712 +1739577600000;2711 +1739664000000;2720 +1739750400000;2721 +1739836800000;2725 +1739923200000;2725 +1740009600000;2725 +1740096000000;2725 +1740182400000;2725 +1740268800000;2736 +1740355200000;2736 +1740441600000;2736 +1740528000000;2735 +1740614400000;2735 +1740700800000;2735 +1740787200000;2735 +1740873600000;2735 +1740960000000;2735 +1741046400000;2734 +1741132800000;2735 +1741219200000;2734 +1741305600000;2735 +1741392000000;2735 +1741478400000;2735 +1741564800000;2737 +1741651200000;2734 +1741737600000;2734 +1741824000000;2734 +1741910400000;2734 +1741996800000;2736 +1742083200000;2736 +1742169600000;2736 +1742256000000;2736 +1742342400000;2738 +1742428800000;2737 +1742515200000;2738 +1742601600000;2738 +1742688000000;2738 +1742774400000;2738 +1742860800000;2737 +1742947200000;2737 +1743033600000;2737 +1743120000000;2737 +1743206400000;2737 +1743292800000;2738 +1743379200000;2738 +1743465600000;2738 +1743552000000;2737 +1743638400000;2737 +1743724800000;2737 +1743811200000;2736 +1743897600000;2733 +1743984000000;2734 +1744070400000;2734 +1744156800000;2684 +1744243200000;2734 +1744329600000;2734 +1744416000000;2734 +1744502400000;2734 +1744588800000;2734 +1744675200000;2734 +1744761600000;2734 +1744848000000;2734 +1744934400000;2733 +1745020800000;2733 +1745107200000;2728 +1745193600000;2729 +1745280000000;2730 +1745366400000;2730 +1745452800000;2730 +1745539200000;2730 +1745625600000;2725 +1745712000000;2725 +1745798400000;2725 +1745884800000;2726 +1745971200000;2728 +1746057600000;2727 +1746144000000;2726 +1746230400000;2725 +1746316800000;2726 +1746403200000;2726 +1746489600000;2726 +1746576000000;2729 +1746662400000;2728 +1746748800000;2729 +1746835200000;2729 +1746921600000;2727 +1747008000000;2725 +1747094400000;2725 +1747180800000;2724 +1747267200000;2713 +1747353600000;2713 +1747440000000;2713 +1747526400000;2717 +1747612800000;2718 +1747699200000;2719 +1747785600000;2720 +1747872000000;2719 +1747958400000;2719 +1748044800000;2719 +1748131200000;2717 +1748217600000;2696 +1748304000000;2696 +1748390400000;2697 +1748476800000;2698 +1748563200000;2698 +1748649600000;2698 +1748736000000;2700 +1748822400000;2701 +1748908800000;2701 +1748995200000;2701 +1749081600000;2701 +1749168000000;2703 +1749254400000;2702 +1749340800000;2702 +1749427200000;2702 +1749513600000;2701 +1749600000000;2701 +1749686400000;2677 +1749772800000;2677 +1749859200000;2678 +1750118400000;2678 +1750204800000;2678 +1750291200000;2678 +1750377600000;2680 +1750464000000;2680 +1750550400000;2680 +1750636800000;2652 +1750723200000;2653 +1750809600000;2654 +1750896000000;2654 +1750982400000;2653 +1751068800000;2654 +1751155200000;2655 +1751241600000;2655 +1751328000000;2655 +1751414400000;2653 +1751500800000;2653 +1751587200000;2653 +1751673600000;2652 +1751760000000;2651 +1751846400000;2651 +1751932800000;2652 +1752019200000;2651 +1752105600000;2651 +1752192000000;2651 +1752278400000;2619 +1752364800000;2619 +1752451200000;2619 +1752537600000;2619 +1752624000000;2619 +1752710400000;2619 +1752796800000;2619 +1752883200000;2619 +1752969600000;2615 +1753056000000;2614 +1753142400000;2614 +1753228800000;2615 +1753315200000;2615 +1753401600000;2615 +1753488000000;2614 +1753574400000;2614 +1753660800000;2613 +1753747200000;2613 +1753833600000;2613 +1753920000000;2613 +1754006400000;2613 +1754092800000;2613 +1754179200000;2610 +1754265600000;2608 +1754352000000;2602 +1754438400000;2602 +1754524800000;2601 +1754611200000;2599 +1754697600000;2598 +1754784000000;2597 +1754870400000;2597 +1754956800000;2597 +1755043200000;2596 +1755129600000;2597 +1755216000000;2597 +1755302400000;2597 +1755388800000;2597 +1755475200000;2598 +1755561600000;2598 +1755648000000;2597 +1755734400000;2550 +1755820800000;2552 +1755907200000;2552 +1755993600000;2552 +1756080000000;2551 +1756166400000;2557 +1756252800000;2557 +1756339200000;2557 +1756425600000;2557 +1756512000000;2556 +1756598400000;2553 +1756684800000;2548 +1756771200000;2544 +1756857600000;2544 +1756944000000;2542 +1757030400000;2544 +1757116800000;2545 +1757203200000;2545 +1757289600000;2545 +1757376000000;2545 +1757462400000;2545 +1757548800000;2545 +1757635200000;2545 +1757721600000;2545 +1757808000000;2546 +1757894400000;2546 +1757980800000;2571 +1758067200000;2571 +1758153600000;2570 +1758240000000;2570 +1758326400000;2571 +1758412800000;2567 +1758499200000;2568 +1758585600000;2567 +1758672000000;2566 +1758758400000;2565 +1758844800000;2563 +1758931200000;2563 +1759017600000;2563 +1759104000000;2563 +1759190400000;2562 +1759276800000;2562 +1759363200000;2562 +1759449600000;2561 +1759536000000;2562 +1759622400000;2562 +1759708800000;2560 +1759795200000;2561 +1759881600000;2543 +1759968000000;2543 +1760054400000;2543 +1760140800000;2543 +1760227200000;2544 +1760313600000;2531 +1760400000000;2532 +1760486400000;2532 +1760572800000;2532 +1760659200000;2532 +1760745600000;2532 +1760832000000;2492 +1760918400000;2491 +1761004800000;2490 +1761091200000;2494 +1761177600000;2488 +1761264000000;2488 +1761350400000;2490 +1761436800000;2490 +1761523200000;2490 +1761609600000;2489 +1761696000000;2489 +1761782400000;2489 +1761868800000;2473 +1761955200000;2473 +1762041600000;2473 +1762128000000;2473 +1762214400000;2461 +1762300800000;2456 +1762387200000;2454 +1762473600000;2454 +1762560000000;2454 +1762646400000;2453 +1762732800000;2454 +1762819200000;2453 +1762905600000;2453 +1762992000000;2452 +1763078400000;2451 +1763164800000;2450 +1763251200000;2450 +1763337600000;2447 +1763424000000;2448 +1763510400000;2449 +1763596800000;2449 +1763683200000;2449 +1763769600000;2449 +1763856000000;2448 +1763942400000;2448 +1764028800000;2432 +1764115200000;2433 +1764201600000;2434 +1764288000000;2434 +1764374400000;2434 +1764460800000;2435 +1764547200000;2435 +1764633600000;2435 +1764720000000;2435 +1764806400000;2433 +1764892800000;2433 +1764979200000;2376 +1765065600000;2376 +1765152000000;2376 +1765238400000;2376 +1765324800000;2376 +1765411200000;2373 +1765497600000;2373 +1765584000000;2373 +1765670400000;2377 +1765756800000;2377 +1765843200000;2377 +1765929600000;2377 +1766016000000;2377 +1766102400000;2377 +1766188800000;2377 +1766275200000;2379 +1766361600000;2379 +1766448000000;2379 +1766534400000;2379 +1766620800000;2379 +1766707200000;2363 +1766793600000;2363 +1766880000000;2363 +1766966400000;2363 +1767052800000;2363 +1767139200000;2366 +1767225600000;2366 +1767312000000;2365 +1767398400000;2365 +1767484800000;2365 +1767571200000;2365 +1767657600000;2365 +1767744000000;2365 +1767830400000;2361 +1767916800000;2362 +1768003200000;2362 +1768089600000;2362 +1768176000000;2360 +1768262400000;2361 +1768348800000;2361 +1768435200000;2361 +1768521600000;2361 +1768608000000;2362 +1768694400000;2362 +1768780800000;2362 +1768867200000;2362 +1768953600000;2362 +1769040000000;2362 +1769126400000;2363 +1769212800000;2363 +1769299200000;2362 +1769385600000;2362 +1769472000000;2357 +1769558400000;2357 +1769644800000;2357 +1769731200000;2356 +1769817600000;2356 +1769904000000;2354 +1769990400000;2356 +1770076800000;2352 +1770163200000;2347 +1770249600000;2348 +1770336000000;2347 +1770422400000;2347 +1770508800000;2346 +1770595200000;2346 +1770681600000;2347 +1770768000000;2347 +1770854400000;2347 +1770940800000;2346 +1771027200000;2348 +1771113600000;2349 +1771200000000;2349 +1771286400000;2349 +1771372800000;2353 +1771459200000;2352 +1771545600000;2352 +1771632000000;2347 +1771718400000;2347 +1771804800000;2347 +1771891200000;2347 +1771977600000;2349 +1772064000000;2349 +1772150400000;2281 +1772236800000;2281 +1772323200000;2280 +1772409600000;2283 +1772496000000;2283 +1772582400000;2283 +1772668800000;2284 +1772755200000;2284 +1772841600000;2284 +1772928000000;2284 +1773014400000;2285 +1773100800000;2285 +1773187200000;2286 +1773273600000;2286 +1773360000000;2286 +1773446400000;2286 +1773532800000;2286 +1773619200000;2286 +1773705600000;2286 +1773792000000;2285 +1773878400000;2285 +1773964800000;2285 +1774051200000;2285 +1774137600000;2286 +1774224000000;2286 +1774310400000;2286 +1774396800000;2292 +1774483200000;2292 +1774569600000;2293 +1774656000000;2294 +1774742400000;2294 +1774828800000;2295 +1774915200000;2296 +1775001600000;2296 +1775088000000;2296 +1775174400000;2296 +1775260800000;2296 +1775347200000;2296 +1775433600000;2296 +1775520000000;2296 +1775606400000;2296 +1775692800000;2296 +1775779200000;2296 +1775865600000;2296 +1775952000000;2296 +1776038400000;2296 +1776124800000;2296 +1776211200000;2291 +1776297600000;2291 +1776384000000;2293 +1776470400000;2293 +1776556800000;2296 +1776643200000;2296 +1776729600000;2297 +1776816000000;2297 +1776902400000;2297 diff --git a/lib/masternodeCountRepo.js b/lib/masternodeCountRepo.js new file mode 100644 index 0000000..58c9bd8 --- /dev/null +++ b/lib/masternodeCountRepo.js @@ -0,0 +1,97 @@ +// Thin repo over the `masternode_count_daily` table (see migration 001). +// +// Owns all SQL for the historical masternode-count time series so +// the writer (services/mnCountLogger.js), the seeder +// (lib/mnCountSeed.js) and the reader (routes/mnCount.js) can share +// one vocabulary and one set of prepared statements instead of +// scattering raw SQL across three files. +// +// Two design choices worth calling out: +// +// 1. INSERT OR IGNORE, not INSERT. The PK on `date` makes the write +// idempotent per UTC calendar day. A restart-storm, a catch-up +// that overlaps with the regular midnight tick, or a seed that +// is re-run on an already-populated table all collapse to a +// no-op rather than either throwing a UNIQUE violation (which +// callers would have to branch on) or overwriting the original +// recorded_at (which we deliberately preserve as audit +// provenance — the first sample wins). +// +// 2. getAll() returns `{ date, users }` rows, not `{ date, total }`. +// That is the shape the FE chart (TrendChart.js) already speaks +// and the shape the retired CSV reader produced. The table +// column is named `total` because that's what Core's +// masternode_count RPC calls it; the projection happens here so +// the route handler stays a thin pass-through. + +function createMasternodeCountRepo(db) { + if (!db) throw new Error('createMasternodeCountRepo: db is required'); + + const insertStmt = db.prepare( + `INSERT OR IGNORE INTO masternode_count_daily (date, total, recorded_at) + VALUES (?, ?, ?)` + ); + + const selectAllStmt = db.prepare( + `SELECT date, total + FROM masternode_count_daily + ORDER BY date ASC` + ); + + const selectLatestDateStmt = db.prepare( + `SELECT date + FROM masternode_count_daily + ORDER BY date DESC + LIMIT 1` + ); + + const countStmt = db.prepare( + `SELECT COUNT(*) AS n FROM masternode_count_daily` + ); + + function validateDate(date) { + if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new Error( + `masternodeCountRepo: date must be 'YYYY-MM-DD', got ${JSON.stringify(date)}` + ); + } + } + + function validateTotal(total) { + if (!Number.isInteger(total) || total < 0) { + throw new Error( + `masternodeCountRepo: total must be a non-negative integer, got ${JSON.stringify(total)}` + ); + } + } + + // Returns { inserted: boolean }. `inserted` is false when the row + // already existed (PK collision swallowed by INSERT OR IGNORE) so + // callers can log "skipped — already recorded" without inspecting + // better-sqlite3 internals. + function upsertByDate(date, total, recordedAt) { + validateDate(date); + validateTotal(total); + const when = + Number.isFinite(recordedAt) && recordedAt >= 0 ? recordedAt : Date.now(); + const info = insertStmt.run(date, total, when); + return { inserted: info.changes > 0 }; + } + + function getAll() { + return selectAllStmt.all().map((r) => ({ date: r.date, users: r.total })); + } + + function getLatestDate() { + const row = selectLatestDateStmt.get(); + return row ? row.date : null; + } + + function isEmpty() { + return countStmt.get().n === 0; + } + + return { upsertByDate, getAll, getLatestDate, isEmpty }; +} + +module.exports = { createMasternodeCountRepo }; diff --git a/lib/mnCountSeed.js b/lib/mnCountSeed.js new file mode 100644 index 0000000..2b064ac --- /dev/null +++ b/lib/mnCountSeed.js @@ -0,0 +1,134 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// One-time seed for `masternode_count_daily` from the committed +// historical CSV at db/seeds/masternode-count.csv. +// +// Why this exists as code rather than a SQL migration: +// * The historical dataset (~2900 rows) is data, not schema. +// Inlining it into a migration would bloat the diff, make review +// miserable, and tie schema changes to data corrections. +// * Making the seed code idempotent lets us re-run on every boot +// without branching on "first ever boot" state — the run is a +// no-op once the table is populated, and `INSERT OR IGNORE` in +// the repo guarantees a correctly-seeded table stays correct +// even if a CSV is edited to include already-present dates. +// +// CSV format (legacy, unchanged from the retired standalone script): +// Header: `Timestamp;Amount` +// Rows: `;` +// +// Parser rules: +// * Semicolon delimiter (the legacy format). +// * A blank trailing line is expected and ignored. +// * Any row that fails integer parsing is skipped with a warn log +// rather than aborting the seed — a single corrupt row should +// not block an otherwise-valid history from loading. + +const DEFAULT_SEED_PATH = path.join( + __dirname, + '..', + 'db', + 'seeds', + 'masternode-count.csv' +); + +function utcDateString(ms) { + return new Date(ms).toISOString().slice(0, 10); +} + +// Generator so the transaction body can pull rows one at a time +// without materializing the whole parsed array. Keeps peak memory +// bounded even if the seed grows into millions of rows. +function* parseSeedCsv(text, log) { + const lines = text.split(/\r?\n/); + let headerSeen = false; + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + if (raw == null) continue; + const line = raw.trim(); + if (!line) continue; + + if (!headerSeen) { + headerSeen = true; + // Accept either a literal header row or a data row; if the + // first non-blank line is numeric we treat it as data (some + // hand-edited copies of the file lose the header). + if (/^[A-Za-z]/.test(line)) continue; + } + + const parts = line.split(';'); + if (parts.length !== 2) { + if (log) log('warn', 'mncount_seed_skip', { line: i + 1, reason: 'shape' }); + continue; + } + const ts = Number(parts[0]); + const total = Number(parts[1]); + if ( + !Number.isFinite(ts) || + ts <= 0 || + !Number.isInteger(total) || + total < 0 + ) { + if (log) log('warn', 'mncount_seed_skip', { line: i + 1, reason: 'values' }); + continue; + } + yield { date: utcDateString(ts), total, recordedAt: ts }; + } +} + +// Runs the seed under `opts.db.transaction(...)` so either every row +// loads or none do — a partial seed (parser throws halfway) is worse +// than no seed, because the daily writer only catches up to "today" +// and would never fill the gap. +function seedMasternodeCount({ + db, + repo, + seedPath = DEFAULT_SEED_PATH, + log = () => {}, + readFile = fs.readFileSync, +} = {}) { + if (!db) throw new Error('seedMasternodeCount: db is required'); + if (!repo) throw new Error('seedMasternodeCount: repo is required'); + + if (!repo.isEmpty()) { + return { seeded: false, reason: 'not-empty', inserted: 0 }; + } + + let text; + try { + text = readFile(seedPath, 'utf8'); + } catch (err) { + log('warn', 'mncount_seed_missing', { + path: seedPath, + err: err && err.message, + }); + return { seeded: false, reason: 'missing', inserted: 0 }; + } + + let inserted = 0; + let skipped = 0; + const txn = db.transaction(() => { + for (const row of parseSeedCsv(text, log)) { + const { inserted: did } = repo.upsertByDate( + row.date, + row.total, + row.recordedAt + ); + if (did) inserted++; + else skipped++; + } + }); + txn(); + + log('info', 'mncount_seed_loaded', { inserted, skipped, seedPath }); + return { seeded: true, reason: 'loaded', inserted, skipped }; +} + +module.exports = { + seedMasternodeCount, + parseSeedCsv, + DEFAULT_SEED_PATH, +}; diff --git a/lib/mnCountSeed.test.js b/lib/mnCountSeed.test.js new file mode 100644 index 0000000..b0d5c33 --- /dev/null +++ b/lib/mnCountSeed.test.js @@ -0,0 +1,161 @@ +'use strict'; + +const path = require('path'); +const { openDatabase } = require('./db'); +const { createMasternodeCountRepo } = require('./masternodeCountRepo'); +const { + seedMasternodeCount, + parseSeedCsv, + DEFAULT_SEED_PATH, +} = require('./mnCountSeed'); + +describe('parseSeedCsv', () => { + test('parses legacy Timestamp;Amount header + rows into UTC date rows', () => { + // 1526425200000 = 2018-05-15T23:00:00Z in the legacy feed. The + // parser does NOT force the timestamp to UTC midnight; it trusts + // the CSV's value and only uses it to project a YYYY-MM-DD. That + // projection must use UTC so environments in non-UTC timezones + // produce identical output. + const csv = 'Timestamp;Amount\n1526425200000;820\n1526511600000;823\n'; + const rows = Array.from(parseSeedCsv(csv)); + expect(rows).toEqual([ + { date: '2018-05-15', total: 820, recordedAt: 1526425200000 }, + { date: '2018-05-16', total: 823, recordedAt: 1526511600000 }, + ]); + }); + + test('skips malformed rows but keeps good ones, logging each skip', () => { + const calls = []; + const log = (level, event, meta) => calls.push({ level, event, meta }); + const csv = [ + 'Timestamp;Amount', + '1526425200000;820', + 'not-a-row', + '1526511600000;-5', + '1526598000000;abc', + '1526684400000;843', + '', + ].join('\n'); + const rows = Array.from(parseSeedCsv(csv, log)); + expect(rows.map((r) => r.total)).toEqual([820, 843]); + expect(calls.filter((c) => c.event === 'mncount_seed_skip')).toHaveLength(3); + }); + + test('accepts a headerless CSV (starts with a data row)', () => { + const csv = '1526425200000;820\n1526511600000;823\n'; + const rows = Array.from(parseSeedCsv(csv)); + expect(rows).toHaveLength(2); + }); + + test('handles CRLF line endings', () => { + const csv = 'Timestamp;Amount\r\n1526425200000;820\r\n1526511600000;823\r\n'; + const rows = Array.from(parseSeedCsv(csv)); + expect(rows).toHaveLength(2); + }); +}); + +describe('seedMasternodeCount', () => { + let db; + let repo; + + beforeEach(() => { + db = openDatabase(':memory:'); + repo = createMasternodeCountRepo(db); + }); + + afterEach(() => db.close()); + + test('loads the full CSV into an empty table, reports inserted count', () => { + const csv = 'Timestamp;Amount\n1526425200000;820\n1526511600000;823\n'; + const result = seedMasternodeCount({ + db, + repo, + readFile: () => csv, + }); + expect(result).toEqual({ + seeded: true, + reason: 'loaded', + inserted: 2, + skipped: 0, + }); + expect(repo.getAll()).toEqual([ + { date: '2018-05-15', users: 820 }, + { date: '2018-05-16', users: 823 }, + ]); + }); + + test('is a no-op when the table already has rows (second boot)', () => { + repo.upsertByDate('2020-01-01', 1000, 1577836800000); + const csv = 'Timestamp;Amount\n1526425200000;820\n'; + const result = seedMasternodeCount({ + db, + repo, + readFile: () => csv, + }); + expect(result).toEqual({ seeded: false, reason: 'not-empty', inserted: 0 }); + // The pre-existing row is untouched and no seed rows bled in. + expect(repo.getAll()).toEqual([{ date: '2020-01-01', users: 1000 }]); + }); + + test('missing seed file reports reason="missing" without throwing', () => { + const result = seedMasternodeCount({ + db, + repo, + seedPath: '/nope/does/not/exist.csv', + readFile: () => { + const err = new Error('ENOENT'); + err.code = 'ENOENT'; + throw err; + }, + }); + expect(result.seeded).toBe(false); + expect(result.reason).toBe('missing'); + expect(repo.isEmpty()).toBe(true); + }); + + test('runs inside a single transaction: a mid-stream throw rolls the whole seed back', () => { + // Force `upsertByDate` to throw on the second call. If the seed + // were row-by-row without a transaction, the first row would + // persist — which would poison `isEmpty()` on the next boot and + // block a retry from ever completing. A transaction makes the + // seed all-or-nothing. + const csv = 'Timestamp;Amount\n1526425200000;820\n1526511600000;823\n'; + const wrappedRepo = { + ...repo, + isEmpty: () => repo.isEmpty(), + upsertByDate: jest + .fn() + .mockImplementationOnce((d, t, r) => repo.upsertByDate(d, t, r)) + .mockImplementationOnce(() => { + throw new Error('synthetic parser failure'); + }), + }; + + expect(() => + seedMasternodeCount({ db, repo: wrappedRepo, readFile: () => csv }) + ).toThrow(/synthetic parser failure/); + expect(repo.getAll()).toEqual([]); + }); + + test('DEFAULT_SEED_PATH points at the in-repo CSV under db/seeds/', () => { + expect(DEFAULT_SEED_PATH.endsWith(path.join('db', 'seeds', 'masternode-count.csv'))).toBe(true); + }); + + test('seeding the real committed CSV loads a dense, sorted history', () => { + // Smoke-check the actual file so a silent corruption of the seed + // is caught here instead of surprising someone six months later. + const result = seedMasternodeCount({ db, repo }); + expect(result.seeded).toBe(true); + expect(result.inserted).toBeGreaterThan(2800); + const rows = repo.getAll(); + // Rows must be sorted ascending and cover a plausible window. + expect(rows[0].date.startsWith('2018-')).toBe(true); + for (let i = 1; i < rows.length; i++) { + expect(rows[i].date >= rows[i - 1].date).toBe(true); + } + // No duplicates: PK on `date` would have made a dup throw; here + // we verify the distinct count. + const distinctDates = new Set(rows.map((r) => r.date)); + expect(distinctDates.size).toBe(rows.length); + }); +}); diff --git a/routes/csvParser.js b/routes/csvParser.js deleted file mode 100644 index cab3858..0000000 --- a/routes/csvParser.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require("express"); -const fs = require("fs"); -const Papa = require("papaparse"); -const router = express.Router(); - -router.get("/mnCount", (req, res) => { - fs.readFile("/root/sysnode/data.csv", "utf8", (err, data) => { - if (err) return res.status(500).send(err); - - const cleaned = data.replace(/\r\n/g, "\n"); - Papa.parse(cleaned, { - delimiter: ";", - header: true, - skipEmptyLines: true, - transformHeader: h => h.trim().toLowerCase().replace(/\W/g, "_"), - complete: results => { - const transformed = results.data - .filter(row => row.timestamp && row.amount) - .map(row => ({ - date: new Date(parseInt(row.timestamp)).toISOString().split("T")[0], - users: parseInt(row.amount) - })); - res.json(transformed); - } - }); - }); -}); - -module.exports = router; \ No newline at end of file diff --git a/routes/mnCount.js b/routes/mnCount.js new file mode 100644 index 0000000..9d4c9b9 --- /dev/null +++ b/routes/mnCount.js @@ -0,0 +1,44 @@ +'use strict'; + +const express = require('express'); + +// GET /mnCount +// ------------ +// Historical daily total of masternodes on the network, used by the +// TrendChart component on sysnode-info's homepage. +// +// Shape (preserved from the retired CSV route for FE compatibility): +// [ { date: 'YYYY-MM-DD', users: }, ... ] +// +// Source: `masternode_count_daily` SQLite table, written once per UTC +// day by services/mnCountLogger.js. The legacy file-backed route +// (routes/csvParser.js reading /root/sysnode/data.csv) is retired — +// it couldn't survive a fresh deploy where the CSV hadn't been +// provisioned, which is exactly how we lost the chart on staging. +// +// Error handling: a DB read failure returns 500 with a stable body so +// the FE's error banner stays predictable. An empty table returns 200 +// with `[]` — that's the correct representation of "we know of no +// history yet" and keeps the TrendChart in its "no data" state +// instead of the "network error" state. + +function createMnCountRouter({ repo, log = () => {} } = {}) { + if (!repo) { + throw new Error('createMnCountRouter: repo is required'); + } + const router = express.Router(); + + router.get('/mnCount', (_req, res) => { + try { + const rows = repo.getAll(); + res.json(rows); + } catch (err) { + log('error', 'mncount_read_failed', { err: err && err.message }); + res.status(500).json({ error: 'internal' }); + } + }); + + return router; +} + +module.exports = { createMnCountRouter }; diff --git a/routes/mnCount.test.js b/routes/mnCount.test.js new file mode 100644 index 0000000..cb10a59 --- /dev/null +++ b/routes/mnCount.test.js @@ -0,0 +1,70 @@ +'use strict'; + +const express = require('express'); +const request = require('supertest'); +const { openDatabase } = require('../lib/db'); +const { createMasternodeCountRepo } = require('../lib/masternodeCountRepo'); +const { createMnCountRouter } = require('./mnCount'); + +function mountApp(router) { + const app = express(); + app.use(router); + return app; +} + +describe('GET /mnCount', () => { + let db; + let repo; + + beforeEach(() => { + db = openDatabase(':memory:'); + repo = createMasternodeCountRepo(db); + }); + + afterEach(() => db.close()); + + test('empty table → 200 with []', async () => { + const app = mountApp(createMnCountRouter({ repo })); + const res = await request(app).get('/mnCount'); + expect(res.status).toBe(200); + expect(res.body).toEqual([]); + }); + + test('populated table → 200 with ascending [{date, users}] rows', async () => { + repo.upsertByDate('2024-03-15', 2200, Date.parse('2024-03-15T00:00:05Z')); + repo.upsertByDate('2024-03-14', 2199, Date.parse('2024-03-14T00:00:05Z')); + repo.upsertByDate('2024-03-16', 2201, Date.parse('2024-03-16T00:00:05Z')); + + const app = mountApp(createMnCountRouter({ repo })); + const res = await request(app).get('/mnCount'); + expect(res.status).toBe(200); + expect(res.body).toEqual([ + { date: '2024-03-14', users: 2199 }, + { date: '2024-03-15', users: 2200 }, + { date: '2024-03-16', users: 2201 }, + ]); + }); + + test('repo read failure → 500 {error: "internal"} and logs the failure', async () => { + const logs = []; + const blowingRepo = { + getAll: () => { + throw new Error('db corrupt'); + }, + }; + const app = mountApp( + createMnCountRouter({ + repo: blowingRepo, + log: (level, event, meta) => logs.push({ level, event, meta }), + }) + ); + const res = await request(app).get('/mnCount'); + expect(res.status).toBe(500); + expect(res.body).toEqual({ error: 'internal' }); + expect(logs.some((l) => l.event === 'mncount_read_failed')).toBe(true); + }); + + test('constructor rejects missing repo', () => { + expect(() => createMnCountRouter({})).toThrow(/repo is required/); + }); +}); diff --git a/server.js b/server.js index e412545..53d5f69 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,7 @@ require('./services/masternodeTracker'); const mnStatsRoute = require('./routes/mnStats'); const masternodesRoute = require('./routes/masternodes'); const governanceRoute = require('./routes/governance'); -const csvParserRoute = require('./routes/csvParser'); +const { createMnCountRouter } = require('./routes/mnCount'); const mnListRoute = require('./routes/mnList'); const mnSearchRoute = require('./routes/mnSearch'); @@ -39,6 +39,11 @@ const { createDefaultSyscoinClient, } = require('./lib/proposalPsbt'); const { createPaliChainGuard } = require('./lib/paliChainGuard'); +const { + createMasternodeCountRepo, +} = require('./lib/masternodeCountRepo'); +const { seedMasternodeCount } = require('./lib/mnCountSeed'); +const { createMnCountLogger } = require('./services/mnCountLogger'); // Per-process cache for `gobject_getcurrentvotes`. Concurrent callers // hitting GET /gov/receipts for the same proposal share one RPC; a @@ -120,6 +125,42 @@ app.use((req, res, next) => { const dbPath = process.env.SYSNODE_DB_PATH || './data/sysnode.db'; const db = openDatabase(dbPath); +// Historical masternode-count store (feeds the /mnCount endpoint + +// the homepage TrendChart). We construct the repo up front because +// three independent callers need it: the one-time seeder, the daily +// logger that appends new rows, and the /mnCount HTTP route. +// +// seedMasternodeCount is idempotent: it loads the committed CSV +// (db/seeds/masternode-count.csv) only when the table is empty, so +// normal restarts and second-boot upgrades both no-op correctly. +const mnCountRepo = createMasternodeCountRepo(db); +seedMasternodeCount({ + db, + repo: mnCountRepo, + log: (level, event, meta) => { + // eslint-disable-next-line no-console + console.log(`[mncount-seed] ${level} ${event}`, meta || ''); + }, +}); + +// Daily masternode-count logger. Catches up on boot if today's row +// is missing, then re-arms for each subsequent midnight UTC. The +// timer is .unref()'d inside the service so the event loop is free +// to exit on SIGINT even if a tick is scheduled. +const mnCountLogger = createMnCountLogger({ + repo: mnCountRepo, + fetchTotal: async () => { + const r = await rpcServices(client.callRpc).masternode_count().call(); + const total = r && r.total; + return Number.isInteger(total) ? total : Number(total); + }, + log: (level, event, meta) => { + // eslint-disable-next-line no-console + console.log(`[mncount] ${level} ${event}`, meta || ''); + }, +}); +mnCountLogger.start(); + // Boot-time config sanity checks. These throw synchronously so a // misconfigured deploy crashes on startup rather than silently turning // every login into a 401 (Codex round-7 P1 on pepper) or dropping mail @@ -308,7 +349,15 @@ mountAuthAndVault(app, { app.use(mnStatsRoute); app.use(masternodesRoute); app.use(governanceRoute); -app.use(csvParserRoute); +app.use( + createMnCountRouter({ + repo: mnCountRepo, + log: (level, event, meta) => { + // eslint-disable-next-line no-console + console.log(`[mncount-route] ${level} ${event}`, meta || ''); + }, + }) +); app.use(mnListRoute); app.use(mnSearchRoute); diff --git a/services/mnCountLogger.js b/services/mnCountLogger.js new file mode 100644 index 0000000..d996a81 --- /dev/null +++ b/services/mnCountLogger.js @@ -0,0 +1,250 @@ +'use strict'; + +// Daily masternode-count logger. +// ------------------------------- +// Appends one row per UTC calendar day to `masternode_count_daily`, +// replacing the retired standalone `mnCount.js` daemon that wrote a +// CSV under /root/sysnode/ via hardcoded RPC credentials. +// +// Behaviour summary: +// +// * On start(): kick a catch-up sample immediately if today's UTC +// row is missing. The old script could only ever fire on its own +// schedule; a restart between midnights meant that day was lost. +// Catch-up closes that hole for any restart that lands before +// the same UTC day rolls over. +// +// * Regular tick: runs at 00:00:05 UTC (the +5s is intentional — +// it protects against a ~second of clock skew causing the timer +// to fire microseconds BEFORE the new day, so `new Date()` still +// reports yesterday and we'd stamp the wrong row). INSERT OR +// IGNORE in the repo makes the ±1s choice irrelevant for +// correctness but not for observability; we prefer "the right +// date" over "a warning log". +// +// * Errors on the scheduled tick (RPC timeout, syscoind restart, +// repo transient lock) back off exponentially from 60s up to +// 1h, capped separately at "do not wait longer than the next +// midnight". A persistent failure therefore retries every hour +// but never skips past the next day's boundary without at least +// one attempt at it. +// +// * stop() + __resetForTests() mirror the ergonomics of +// services/sysMain.js so test harnesses can drive the logger +// deterministically with fake timers. +// +// Non-goals: +// +// * No backfill of intermediate missed days. Core does not expose +// historical masternode counts; a linear interpolation would be +// a lie and a constant-fill would be misleading. A gap is +// visible on the chart as a flat segment between the two +// bracketing points — the truthful representation. + +const BASE_RETRY_MS = 60 * 1000; +const MAX_RETRY_MS = 60 * 60 * 1000; +const POST_MIDNIGHT_SKEW_MS = 5 * 1000; + +function utcDateString(ms) { + return new Date(ms).toISOString().slice(0, 10); +} + +// Milliseconds from `fromMs` until (next-UTC-midnight + POST_MIDNIGHT_SKEW_MS), +// clamped to at least 1s so we never recurse synchronously if the +// clock is pathological. +function msUntilNextMidnightUtc(fromMs) { + const d = new Date(fromMs); + const nextMidnight = Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate() + 1, + 0, + 0, + 0, + 0 + ); + return Math.max(1000, nextMidnight + POST_MIDNIGHT_SKEW_MS - fromMs); +} + +function createMnCountLogger({ + repo, + fetchTotal, + now = () => Date.now(), + log = () => {}, + setTimeoutImpl = setTimeout, + clearTimeoutImpl = clearTimeout, +} = {}) { + if (!repo) throw new Error('createMnCountLogger: repo is required'); + if (typeof fetchTotal !== 'function') { + throw new Error('createMnCountLogger: fetchTotal must be a function'); + } + + let timer = null; + let stopped = false; + let started = false; + let lastWriteAt = 0; + let lastWriteDate = null; + let lastError = null; + let currentRetryMs = BASE_RETRY_MS; + + async function sampleAndWrite(label) { + const total = await fetchTotal(); + if (!Number.isInteger(total) || total < 0) { + throw new Error( + `fetchTotal returned a non-integer or negative value: ${JSON.stringify(total)}` + ); + } + const ts = now(); + const date = utcDateString(ts); + const result = repo.upsertByDate(date, total, ts); + lastWriteAt = ts; + lastWriteDate = date; + lastError = null; + currentRetryMs = BASE_RETRY_MS; + log('info', 'mncount_write', { + label, + date, + total, + inserted: result.inserted, + }); + return { date, total, inserted: result.inserted }; + } + + function schedule(ms) { + if (stopped) return; + if (timer) { + clearTimeoutImpl(timer); + timer = null; + } + timer = setTimeoutImpl(() => { + timer = null; + // Fire-and-forget: runAndReschedule owns its own rescheduling + // and error handling. + runAndReschedule(); + }, ms); + if (timer && typeof timer.unref === 'function') timer.unref(); + } + + async function runAndReschedule() { + if (stopped) return; + + // Fast-path: if today's row is already there (boot after a + // successful earlier tick, or a spurious re-fire on the same + // UTC day) skip the RPC entirely and arm for next midnight. + // The INSERT OR IGNORE in the repo would collapse a duplicate + // write anyway, but avoiding the RPC call keeps Core's load + // bounded and stops a same-day re-sample from shadowing the + // 00:00 snapshot with an afternoon value at the log layer. + const today = utcDateString(now()); + if (repo.getLatestDate() === today) { + if (stopped) return; + schedule(msUntilNextMidnightUtc(now())); + return; + } + + try { + await sampleAndWrite('tick'); + if (stopped) return; + schedule(msUntilNextMidnightUtc(now())); + } catch (err) { + lastError = err && err.message; + log('error', 'mncount_tick_failed', { err: err && err.message }); + currentRetryMs = Math.min(currentRetryMs * 2, MAX_RETRY_MS); + // Never overshoot the next day's midnight: we want at least + // one scheduled attempt BEFORE the next UTC-day boundary so a + // single bad sample cannot lose its day entirely. + const untilMidnight = msUntilNextMidnightUtc(now()); + if (stopped) return; + schedule(Math.min(currentRetryMs, untilMidnight)); + } + } + + // Exposed for tests / one-shot callers that just want the + // "sample today if missing" decision without starting the + // scheduler loop. The production boot path funnels through + // runAndReschedule() instead so a boot-time failure is retried + // with backoff before midnight (Codex PR16 P2). + async function catchUpIfNeeded() { + const today = utcDateString(now()); + const latest = repo.getLatestDate(); + if (latest === today) { + return { skipped: true, reason: 'already-today' }; + } + try { + const out = await sampleAndWrite('catchup'); + return { skipped: false, ...out }; + } catch (err) { + lastError = err && err.message; + log('error', 'mncount_catchup_failed', { err: err && err.message }); + return { skipped: true, reason: 'error', err: err && err.message }; + } + } + + function start() { + if (started) return; + started = true; + stopped = false; + // Route the boot path through runAndReschedule() so all three + // outcomes are handled uniformly by the scheduler: + // * today already recorded → arm for next midnight. + // * sample succeeds now → write, arm for next midnight. + // * sample fails now → backoff retry, clamped to + // stay inside this UTC day. + // The previous arrangement always armed for next midnight after + // catch-up, so a transient RPC blip at boot would lose today + // permanently instead of retrying (Codex PR16 P2). + runAndReschedule().catch((err) => { + lastError = err && err.message; + log('error', 'mncount_start_failed', { err: err && err.message }); + }); + } + + function stop() { + stopped = true; + started = false; + if (timer) { + clearTimeoutImpl(timer); + timer = null; + } + } + + function getDiagnostics() { + return { + started, + stopped, + lastWriteAt, + lastWriteDate, + lastError, + currentRetryMs, + hasPendingTimer: timer !== null, + }; + } + + function __resetForTests() { + stop(); + stopped = false; + started = false; + lastWriteAt = 0; + lastWriteDate = null; + lastError = null; + currentRetryMs = BASE_RETRY_MS; + } + + return { + start, + stop, + catchUpIfNeeded, + runAndReschedule, + getDiagnostics, + __resetForTests, + }; +} + +module.exports = { + createMnCountLogger, + utcDateString, + msUntilNextMidnightUtc, + BASE_RETRY_MS, + MAX_RETRY_MS, + POST_MIDNIGHT_SKEW_MS, +}; diff --git a/services/mnCountLogger.test.js b/services/mnCountLogger.test.js new file mode 100644 index 0000000..2de41a9 --- /dev/null +++ b/services/mnCountLogger.test.js @@ -0,0 +1,439 @@ +'use strict'; + +const { openDatabase } = require('../lib/db'); +const { createMasternodeCountRepo } = require('../lib/masternodeCountRepo'); +const { + createMnCountLogger, + utcDateString, + msUntilNextMidnightUtc, + BASE_RETRY_MS, + MAX_RETRY_MS, + POST_MIDNIGHT_SKEW_MS, +} = require('./mnCountLogger'); + +// Tiny deterministic scheduler injected in place of setTimeout so +// tests can advance time without juggling jest.useFakeTimers state. +// The scheduler never auto-fires; the test calls `fireNext()` to pop +// the next timer and invoke its callback, optionally after advancing +// the clock by the timer's requested delay. +function makeManualScheduler(clock) { + const timers = []; + let nextHandle = 1; + + const setTimeoutImpl = (fn, delay) => { + const handle = nextHandle++; + const t = { handle, fn, delay, dueAt: clock.nowMs + delay, cancelled: false }; + timers.push(t); + return { handle, unref() {} }; + }; + + const clearTimeoutImpl = (token) => { + if (!token) return; + const h = token.handle; + for (const t of timers) if (t.handle === h) t.cancelled = true; + }; + + function pending() { + return timers.filter((t) => !t.cancelled); + } + + async function fireNext({ advanceClock = true } = {}) { + const live = pending(); + if (live.length === 0) throw new Error('no pending timer to fire'); + // The earliest-due live timer wins. + live.sort((a, b) => a.dueAt - b.dueAt); + const t = live[0]; + t.cancelled = true; + if (advanceClock) clock.nowMs = t.dueAt; + await t.fn(); + // Let any follow-up promise chain settle before the test asserts. + await new Promise((r) => setImmediate(r)); + } + + return { + setTimeoutImpl, + clearTimeoutImpl, + pending, + fireNext, + pendingDelays: () => pending().map((t) => t.delay), + pendingDueAts: () => pending().map((t) => t.dueAt), + }; +} + +function msAt(iso) { + return Date.parse(iso); +} + +describe('utcDateString', () => { + test('projects any instant in a UTC day to that YYYY-MM-DD', () => { + expect(utcDateString(msAt('2024-03-15T00:00:00Z'))).toBe('2024-03-15'); + expect(utcDateString(msAt('2024-03-15T12:34:56Z'))).toBe('2024-03-15'); + expect(utcDateString(msAt('2024-03-15T23:59:59Z'))).toBe('2024-03-15'); + // Just past midnight UTC rolls over. + expect(utcDateString(msAt('2024-03-16T00:00:00Z'))).toBe('2024-03-16'); + }); +}); + +describe('msUntilNextMidnightUtc', () => { + test('returns time until next UTC midnight plus the skew buffer', () => { + const fromMs = msAt('2024-03-15T23:59:55Z'); + // Next midnight is 5s away, plus 5s skew = 10s. + expect(msUntilNextMidnightUtc(fromMs)).toBe(10 * 1000); + }); + + test('early in a UTC day waits almost a full day', () => { + const fromMs = msAt('2024-03-15T00:00:06Z'); + // Midnight + skew is (24h - 6s) + 5s = 86399s away. + const expected = 24 * 3600 * 1000 - 6 * 1000 + POST_MIDNIGHT_SKEW_MS; + expect(msUntilNextMidnightUtc(fromMs)).toBe(expected); + }); + + test('never returns less than 1 second even with a pathological clock', () => { + // Exactly at the target — guaranteed non-negative via Math.max. + const targetMs = msAt('2024-03-16T00:00:05Z'); + expect(msUntilNextMidnightUtc(targetMs)).toBeGreaterThanOrEqual(1000); + }); +}); + +describe('createMnCountLogger', () => { + let db; + let repo; + let clock; + let scheduler; + let fetchTotal; + let logs; + let logger; + + function setup({ rpcSequence = [], startAtIso = '2024-03-15T12:00:00Z' } = {}) { + db = openDatabase(':memory:'); + repo = createMasternodeCountRepo(db); + clock = { nowMs: msAt(startAtIso) }; + scheduler = makeManualScheduler(clock); + + let rpcIdx = 0; + fetchTotal = jest.fn(async () => { + if (rpcIdx >= rpcSequence.length) { + throw new Error(`rpcSequence exhausted (idx=${rpcIdx})`); + } + const item = rpcSequence[rpcIdx++]; + if (typeof item === 'function') return item(); + if (item instanceof Error) throw item; + return item; + }); + + logs = []; + logger = createMnCountLogger({ + repo, + fetchTotal, + now: () => clock.nowMs, + log: (level, event, meta) => logs.push({ level, event, meta }), + setTimeoutImpl: scheduler.setTimeoutImpl, + clearTimeoutImpl: scheduler.clearTimeoutImpl, + }); + } + + afterEach(() => { + if (logger) logger.stop(); + if (db) db.close(); + }); + + test('catchUpIfNeeded writes today when the table is empty', async () => { + setup({ rpcSequence: [1234], startAtIso: '2024-03-15T12:00:00Z' }); + const result = await logger.catchUpIfNeeded(); + expect(result).toEqual({ + skipped: false, + date: '2024-03-15', + total: 1234, + inserted: true, + }); + expect(repo.getAll()).toEqual([{ date: '2024-03-15', users: 1234 }]); + }); + + test('catchUpIfNeeded writes today when latest row is older', async () => { + setup({ rpcSequence: [2000], startAtIso: '2024-03-15T06:00:00Z' }); + repo.upsertByDate('2024-03-10', 1000, msAt('2024-03-10T00:00:00Z')); + const result = await logger.catchUpIfNeeded(); + expect(result.skipped).toBe(false); + expect(result.date).toBe('2024-03-15'); + expect(repo.getLatestDate()).toBe('2024-03-15'); + }); + + test('catchUpIfNeeded is a no-op when today is already recorded', async () => { + setup({ rpcSequence: [], startAtIso: '2024-03-15T12:00:00Z' }); + repo.upsertByDate('2024-03-15', 2200, msAt('2024-03-15T00:00:05Z')); + const result = await logger.catchUpIfNeeded(); + expect(result).toEqual({ skipped: true, reason: 'already-today' }); + expect(fetchTotal).not.toHaveBeenCalled(); + }); + + test('catchUpIfNeeded swallows RPC errors and surfaces reason="error"', async () => { + setup({ + rpcSequence: [new Error('rpc down')], + startAtIso: '2024-03-15T12:00:00Z', + }); + const result = await logger.catchUpIfNeeded(); + expect(result.skipped).toBe(true); + expect(result.reason).toBe('error'); + expect(result.err).toBe('rpc down'); + expect(repo.isEmpty()).toBe(true); + expect(logs.some((l) => l.event === 'mncount_catchup_failed')).toBe(true); + }); + + test('repeated writes for the same UTC date are idempotent (skip + PK on date)', async () => { + setup({ rpcSequence: [2200], startAtIso: '2024-03-15T12:00:00Z' }); + await logger.catchUpIfNeeded(); + + // First layer of protection: once today's row is recorded, + // runAndReschedule MUST skip the RPC entirely rather than + // refetch and overwrite. That keeps the 00:00 snapshot + // authoritative and stops a long event-loop stall / spurious + // re-fire from shadowing it with an afternoon value. + clock.nowMs = msAt('2024-03-15T18:00:00Z'); + await logger.runAndReschedule(); + expect(fetchTotal).toHaveBeenCalledTimes(1); + expect(repo.getAll()).toEqual([{ date: '2024-03-15', users: 2200 }]); + + // Second layer of protection: even if the skip logic were ever + // bypassed, the PK on `date` via INSERT OR IGNORE collapses the + // duplicate write without overwriting the first row's total. + repo.upsertByDate('2024-03-15', 9999, msAt('2024-03-15T18:00:00Z')); + expect(repo.getAll()).toEqual([{ date: '2024-03-15', users: 2200 }]); + }); + + test('start() catches up, then schedules the next tick at midnight+skew', async () => { + setup({ rpcSequence: [1500], startAtIso: '2024-03-15T06:00:00Z' }); + logger.start(); + // Let the catch-up microtask chain drain so the .finally() has + // run and posted its timer. + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + + expect(repo.getLatestDate()).toBe('2024-03-15'); + const pending = scheduler.pending(); + expect(pending).toHaveLength(1); + // Catch-up ran at 06:00, so next midnight (+5s) is 18h+5s away. + const expected = 18 * 3600 * 1000 + POST_MIDNIGHT_SKEW_MS; + expect(pending[0].delay).toBe(expected); + }); + + test('start() is idempotent: a second call does not schedule a second timer', async () => { + setup({ rpcSequence: [1500], startAtIso: '2024-03-15T06:00:00Z' }); + logger.start(); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + expect(scheduler.pending()).toHaveLength(1); + expect(fetchTotal).toHaveBeenCalledTimes(1); + }); + + test('start() with today already recorded skips the catch-up RPC call', async () => { + setup({ rpcSequence: [], startAtIso: '2024-03-15T12:00:00Z' }); + repo.upsertByDate('2024-03-15', 2000, msAt('2024-03-15T00:00:05Z')); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + expect(fetchTotal).not.toHaveBeenCalled(); + expect(scheduler.pending()).toHaveLength(1); + }); + + test('start() with RPC down retries inside the same UTC day (Codex PR16 P2)', async () => { + // Boot at 06:00 UTC — 18 hours of headroom until next midnight. + // First RPC call fails (e.g. syscoind still warming up after a + // joint pm2 restart); the logger MUST schedule a retry that + // fires BEFORE midnight rather than silently deferring today's + // sample and losing the row entirely. Previously start() + // always armed for next midnight regardless of catch-up + // outcome, which is exactly the bug Codex flagged. + setup({ + rpcSequence: [new Error('rpc down at boot'), 2500], + startAtIso: '2024-03-15T06:00:00Z', + }); + logger.start(); + for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r)); + + // The scheduled retry must be strictly inside today's UTC + // window, never the 18h-away next-midnight delay. + const pending = scheduler.pending(); + expect(pending).toHaveLength(1); + const eighteenHoursFiveSec = 18 * 3600 * 1000 + POST_MIDNIGHT_SKEW_MS; + expect(pending[0].delay).toBeLessThan(eighteenHoursFiveSec); + expect(pending[0].delay).toBeGreaterThan(0); + expect(repo.isEmpty()).toBe(true); + expect(logs.some((l) => l.event === 'mncount_tick_failed')).toBe(true); + + // Fire the retry: RPC has recovered, today's row is captured, + // and the logger arms for next midnight (not another retry). + await scheduler.fireNext(); + expect(repo.getAll()).toEqual([{ date: '2024-03-15', users: 2500 }]); + const afterSuccess = scheduler.pendingDelays(); + expect(afterSuccess).toHaveLength(1); + // Post-success from roughly 06:02Z, next midnight is ~18h out. + expect(afterSuccess[0]).toBeGreaterThan(17 * 3600 * 1000); + }); + + test('start() keeps backing off on repeated RPC failure, still inside today', async () => { + // Three sequential failures early in a UTC day must all + // schedule retries inside the same day rather than skipping + // ahead to the next midnight. Exponential growth is capped + // by the until-midnight clamp. + setup({ + rpcSequence: [ + new Error('boot RPC fail 1'), + new Error('retry RPC fail 2'), + new Error('retry RPC fail 3'), + ], + startAtIso: '2024-03-15T06:00:00Z', + }); + logger.start(); + for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r)); + + const untilMidnightFromBoot = 18 * 3600 * 1000 + POST_MIDNIGHT_SKEW_MS; + expect(scheduler.pendingDelays()[0]).toBeLessThan(untilMidnightFromBoot); + + await scheduler.fireNext(); // 2nd failure + expect(scheduler.pendingDelays()[0]).toBeLessThan(untilMidnightFromBoot); + + await scheduler.fireNext(); // 3rd failure + expect(scheduler.pendingDelays()[0]).toBeLessThanOrEqual( + msUntilNextMidnightUtc(clock.nowMs) + ); + expect(repo.isEmpty()).toBe(true); + }); + + test('scheduled tick writes the new day and re-arms for the following midnight', async () => { + setup({ + rpcSequence: [1500, 1505], + startAtIso: '2024-03-15T06:00:00Z', + }); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + // Advance to the scheduled firing moment and run the tick. + await scheduler.fireNext(); + + expect(repo.getAll()).toEqual([ + { date: '2024-03-15', users: 1500 }, // catch-up + { date: '2024-03-16', users: 1505 }, // tick + ]); + // After a successful tick, the logger re-arms for the next + // midnight+skew, i.e. roughly 24h out from 2024-03-16T00:00:05Z. + const pending = scheduler.pending(); + expect(pending).toHaveLength(1); + expect(pending[0].delay).toBe(24 * 3600 * 1000); + }); + + test('tick failure backs off exponentially, capped below the next day', async () => { + setup({ + rpcSequence: [ + 1500, + new Error('rpc timeout 1'), + new Error('rpc timeout 2'), + 3000, + ], + startAtIso: '2024-03-15T06:00:00Z', + }); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + + // First scheduled tick fails → expect a retry at BASE_RETRY_MS + // (or the time-to-midnight, whichever is smaller — at 00:00:05Z + // next midnight is 24h away, so 60s wins). + await scheduler.fireNext(); // 1st failure + expect(scheduler.pendingDelays()[0]).toBe(BASE_RETRY_MS * 2); + + // Second failure at the retry moment: backoff doubles again. + await scheduler.fireNext(); + expect(scheduler.pendingDelays()[0]).toBe(BASE_RETRY_MS * 4); + + // Third attempt succeeds: the scheduler now arms for the NEXT + // midnight+skew, not for the retry cadence. + await scheduler.fireNext(); + expect(repo.getLatestDate()).toBe('2024-03-16'); + // From clock.nowMs on 2024-03-16 (just past midnight+skew+some retries), + // the next midnight is ~24h away again. + expect(scheduler.pendingDelays()[0]).toBeGreaterThan(20 * 3600 * 1000); + }); + + test('retry delay is clamped to not exceed the next midnight boundary', async () => { + // Start near the end of a UTC day so msUntilNextMidnightUtc is + // smaller than BASE_RETRY_MS. The first failed tick must choose + // the midnight-clamp over the 60s retry so it still samples the + // new day. + setup({ + rpcSequence: [ + 1500, // catch-up + new Error('rpc down'), // tick at 00:00:05 + ], + // 23:59:30 UTC — catch-up writes for 2024-03-15, timer arms + // for +35s (until 00:00:05 next day). + startAtIso: '2024-03-15T23:59:30Z', + }); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + + // Fire the scheduled tick — it will fail at ~00:00:05. The + // retry choice is min(120_000, msUntilNextMidnight). The next + // midnight (the 17th's) is ~24h away, so 120s wins here — but + // the important invariant is that the retry is NEVER greater + // than the next midnight. Assert that cap rather than a specific + // schedule value so the test stays meaningful. + await scheduler.fireNext(); + const delay = scheduler.pendingDelays()[0]; + expect(delay).toBeLessThanOrEqual(msUntilNextMidnightUtc(clock.nowMs)); + expect(delay).toBeGreaterThan(0); + expect(logs.some((l) => l.event === 'mncount_tick_failed')).toBe(true); + }); + + test('non-integer / negative RPC response is treated as a failure', async () => { + setup({ + rpcSequence: [-3, 42.5], + startAtIso: '2024-03-15T06:00:00Z', + }); + const r1 = await logger.catchUpIfNeeded(); + expect(r1.skipped).toBe(true); + expect(r1.reason).toBe('error'); + + // Second attempt (now a float) also rejects. + const r2 = await logger.catchUpIfNeeded(); + expect(r2.skipped).toBe(true); + expect(r2.reason).toBe('error'); + expect(repo.isEmpty()).toBe(true); + }); + + test('stop() clears the pending timer and prevents re-arming', async () => { + setup({ rpcSequence: [1500], startAtIso: '2024-03-15T06:00:00Z' }); + logger.start(); + for (let i = 0; i < 10; i++) await new Promise((r) => setImmediate(r)); + expect(scheduler.pending()).toHaveLength(1); + + logger.stop(); + expect(scheduler.pending()).toHaveLength(0); + expect(logger.getDiagnostics().stopped).toBe(true); + }); + + test('stop() between start() and catch-up resolution does not leave a timer behind', async () => { + // Make fetchTotal pend so start()'s catchUpIfNeeded is still in flight. + let releaseRpc; + const pending = new Promise((res) => { + releaseRpc = res; + }); + setup({ + rpcSequence: [() => pending], + startAtIso: '2024-03-15T06:00:00Z', + }); + + logger.start(); + // At this point start() has kicked off catch-up but the await + // hasn't resolved; no timer yet. + expect(scheduler.pending()).toHaveLength(0); + + logger.stop(); + releaseRpc(1500); + for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r)); + + expect(scheduler.pending()).toHaveLength(0); + expect(logger.getDiagnostics().stopped).toBe(true); + }); + + test('MAX_RETRY_MS cap keeps the backoff sane on chronic failure', async () => { + // Verify the exposed constants so they cannot regress silently. + expect(BASE_RETRY_MS).toBe(60 * 1000); + expect(MAX_RETRY_MS).toBe(60 * 60 * 1000); + }); +});