diff --git a/.gitignore b/.gitignore index a83917a19..fa3baa3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ yarn-error.log* # Coverage coverage +coverage-*/ *.lcov .nyc_output diff --git a/client/coverage-agentF/coverage-final.json b/client/coverage-agentF/coverage-final.json deleted file mode 100644 index 62f7efc82..000000000 --- a/client/coverage-agentF/coverage-final.json +++ /dev/null @@ -1,3 +0,0 @@ -{"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\services\\photoService.ts": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\services\\photoService.ts","statementMap":{"0":{"start":{"line":9,"column":14},"end":{"line":9,"column":null}},"1":{"start":{"line":10,"column":17},"end":{"line":10,"column":null}},"2":{"start":{"line":11,"column":18},"end":{"line":11,"column":null}},"3":{"start":{"line":13,"column":23},"end":{"line":13,"column":null}},"4":{"start":{"line":17,"column":23},"end":{"line":17,"column":null}},"5":{"start":{"line":18,"column":21},"end":{"line":18,"column":null}},"6":{"start":{"line":19,"column":40},"end":{"line":19,"column":null}},"7":{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},"8":{"start":{"line":23,"column":4},"end":{"line":23,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":26,"column":2},"end":{"line":26,"column":null}},"11":{"start":{"line":26,"column":32},"end":{"line":26,"column":58}},"12":{"start":{"line":30,"column":15},"end":{"line":30,"column":null}},"13":{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},"14":{"start":{"line":32,"column":4},"end":{"line":32,"column":null}},"15":{"start":{"line":34,"column":4},"end":{"line":34,"column":null}},"16":{"start":{"line":39,"column":2},"end":{"line":39,"column":null}},"17":{"start":{"line":39,"column":36},"end":{"line":39,"column":45}},"18":{"start":{"line":40,"column":2},"end":{"line":40,"column":null}},"19":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"20":{"start":{"line":44,"column":41},"end":{"line":44,"column":50}},"21":{"start":{"line":45,"column":2},"end":{"line":45,"column":null}},"22":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"23":{"start":{"line":49,"column":27},"end":{"line":49,"column":null}},"24":{"start":{"line":50,"column":2},"end":{"line":50,"column":null}},"25":{"start":{"line":51,"column":2},"end":{"line":51,"column":null}},"26":{"start":{"line":51,"column":17},"end":{"line":51,"column":48}},"27":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"28":{"start":{"line":56,"column":32},"end":{"line":56,"column":null}},"29":{"start":{"line":57,"column":2},"end":{"line":57,"column":null}},"30":{"start":{"line":58,"column":2},"end":{"line":58,"column":null}},"31":{"start":{"line":58,"column":17},"end":{"line":58,"column":53}},"32":{"start":{"line":62,"column":2},"end":{"line":62,"column":null}},"33":{"start":{"line":66,"column":2},"end":{"line":66,"column":null}},"34":{"start":{"line":71,"column":2},"end":{"line":92,"column":null}},"35":{"start":{"line":72,"column":16},"end":{"line":72,"column":null}},"36":{"start":{"line":73,"column":4},"end":{"line":73,"column":null}},"37":{"start":{"line":74,"column":4},"end":{"line":89,"column":null}},"38":{"start":{"line":75,"column":6},"end":{"line":88,"column":null}},"39":{"start":{"line":76,"column":23},"end":{"line":76,"column":null}},"40":{"start":{"line":77,"column":8},"end":{"line":77,"column":null}},"41":{"start":{"line":78,"column":8},"end":{"line":78,"column":null}},"42":{"start":{"line":79,"column":20},"end":{"line":79,"column":null}},"43":{"start":{"line":80,"column":18},"end":{"line":80,"column":null}},"44":{"start":{"line":81,"column":14},"end":{"line":81,"column":null}},"45":{"start":{"line":82,"column":14},"end":{"line":82,"column":null}},"46":{"start":{"line":83,"column":8},"end":{"line":83,"column":null}},"47":{"start":{"line":84,"column":8},"end":{"line":84,"column":null}},"48":{"start":{"line":85,"column":8},"end":{"line":85,"column":null}},"49":{"start":{"line":86,"column":8},"end":{"line":86,"column":null}},"50":{"start":{"line":87,"column":8},"end":{"line":87,"column":null}},"51":{"start":{"line":88,"column":16},"end":{"line":88,"column":30}},"52":{"start":{"line":90,"column":4},"end":{"line":90,"column":null}},"53":{"start":{"line":90,"column":24},"end":{"line":90,"column":null}},"54":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"55":{"start":{"line":103,"column":17},"end":{"line":103,"column":null}},"56":{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},"57":{"start":{"line":104,"column":16},"end":{"line":104,"column":36}},"58":{"start":{"line":104,"column":36},"end":{"line":104,"column":43}},"59":{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},"60":{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},"61":{"start":{"line":107,"column":18},"end":{"line":107,"column":null}},"62":{"start":{"line":108,"column":4},"end":{"line":108,"column":null}},"63":{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},"64":{"start":{"line":113,"column":30},"end":{"line":113,"column":null}},"65":{"start":{"line":114,"column":4},"end":{"line":114,"column":null}},"66":{"start":{"line":115,"column":4},"end":{"line":115,"column":null}},"67":{"start":{"line":116,"column":4},"end":{"line":116,"column":null}},"68":{"start":{"line":118,"column":4},"end":{"line":120,"column":null}},"69":{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},"70":{"start":{"line":119,"column":19},"end":{"line":119,"column":47}},"71":{"start":{"line":119,"column":47},"end":{"line":119,"column":76}},"72":{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},"73":{"start":{"line":124,"column":2},"end":{"line":124,"column":null}},"74":{"start":{"line":125,"column":2},"end":{"line":157,"column":null}},"75":{"start":{"line":126,"column":4},"end":{"line":156,"column":null}},"76":{"start":{"line":128,"column":25},"end":{"line":128,"column":null}},"77":{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},"78":{"start":{"line":130,"column":36},"end":{"line":130,"column":null}},"79":{"start":{"line":131,"column":10},"end":{"line":131,"column":null}},"80":{"start":{"line":132,"column":10},"end":{"line":132,"column":null}},"81":{"start":{"line":133,"column":10},"end":{"line":133,"column":null}},"82":{"start":{"line":134,"column":10},"end":{"line":134,"column":null}},"83":{"start":{"line":138,"column":34},"end":{"line":138,"column":null}},"84":{"start":{"line":139,"column":8},"end":{"line":139,"column":null}},"85":{"start":{"line":140,"column":8},"end":{"line":140,"column":null}},"86":{"start":{"line":141,"column":8},"end":{"line":141,"column":null}},"87":{"start":{"line":144,"column":22},"end":{"line":144,"column":null}},"88":{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},"89":{"start":{"line":146,"column":10},"end":{"line":146,"column":null}},"90":{"start":{"line":147,"column":10},"end":{"line":147,"column":null}},"91":{"start":{"line":151,"column":34},"end":{"line":151,"column":null}},"92":{"start":{"line":152,"column":8},"end":{"line":152,"column":null}},"93":{"start":{"line":153,"column":8},"end":{"line":153,"column":null}},"94":{"start":{"line":154,"column":8},"end":{"line":154,"column":null}},"95":{"start":{"line":156,"column":23},"end":{"line":156,"column":50}},"96":{"start":{"line":156,"column":50},"end":{"line":156,"column":71}},"97":{"start":{"line":161,"column":36},"end":{"line":161,"column":null}},"98":{"start":{"line":162,"column":2},"end":{"line":164,"column":null}},"99":{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},"100":{"start":{"line":163,"column":24},"end":{"line":163,"column":null}},"101":{"start":{"line":165,"column":2},"end":{"line":165,"column":null}}},"fnMap":{"0":{"name":"acquireRequestSlot","decl":{"start":{"line":21,"column":9},"end":{"line":21,"column":45}},"loc":{"start":{"line":21,"column":45},"end":{"line":27,"column":null}},"line":21},"1":{"name":"(anonymous_1)","decl":{"start":{"line":26,"column":13},"end":{"line":26,"column":21}},"loc":{"start":{"line":26,"column":32},"end":{"line":26,"column":58}},"line":26},"2":{"name":"releaseRequestSlot","decl":{"start":{"line":29,"column":9},"end":{"line":29,"column":36}},"loc":{"start":{"line":29,"column":36},"end":{"line":36,"column":null}},"line":29},"3":{"name":"notify","decl":{"start":{"line":38,"column":9},"end":{"line":38,"column":16}},"loc":{"start":{"line":38,"column":48},"end":{"line":41,"column":null}},"line":38},"4":{"name":"(anonymous_4)","decl":{"start":{"line":39,"column":22},"end":{"line":39,"column":30}},"loc":{"start":{"line":39,"column":36},"end":{"line":39,"column":45}},"line":39},"5":{"name":"notifyThumb","decl":{"start":{"line":43,"column":9},"end":{"line":43,"column":21}},"loc":{"start":{"line":43,"column":49},"end":{"line":46,"column":null}},"line":43},"6":{"name":"(anonymous_6)","decl":{"start":{"line":44,"column":27},"end":{"line":44,"column":35}},"loc":{"start":{"line":44,"column":41},"end":{"line":44,"column":50}},"line":44},"7":{"name":"onPhotoLoaded","decl":{"start":{"line":48,"column":16},"end":{"line":48,"column":30}},"loc":{"start":{"line":48,"column":88},"end":{"line":52,"column":null}},"line":48},"8":{"name":"(anonymous_8)","decl":{"start":{"line":51,"column":2},"end":{"line":51,"column":15}},"loc":{"start":{"line":51,"column":15},"end":{"line":51,"column":null}},"line":51},"9":{"name":"onThumbReady","decl":{"start":{"line":55,"column":16},"end":{"line":55,"column":29}},"loc":{"start":{"line":55,"column":83},"end":{"line":59,"column":null}},"line":55},"10":{"name":"(anonymous_10)","decl":{"start":{"line":58,"column":2},"end":{"line":58,"column":15}},"loc":{"start":{"line":58,"column":15},"end":{"line":58,"column":null}},"line":58},"11":{"name":"getCached","decl":{"start":{"line":61,"column":16},"end":{"line":61,"column":26}},"loc":{"start":{"line":61,"column":63},"end":{"line":63,"column":null}},"line":61},"12":{"name":"isLoading","decl":{"start":{"line":65,"column":16},"end":{"line":65,"column":26}},"loc":{"start":{"line":65,"column":48},"end":{"line":67,"column":null}},"line":65},"13":{"name":"urlToBase64","decl":{"start":{"line":70,"column":16},"end":{"line":70,"column":28}},"loc":{"start":{"line":70,"column":84},"end":{"line":93,"column":null}},"line":70},"14":{"name":"(anonymous_14)","decl":{"start":{"line":71,"column":13},"end":{"line":71,"column":21}},"loc":{"start":{"line":71,"column":32},"end":{"line":92,"column":3}},"line":71},"15":{"name":"(anonymous_15)","decl":{"start":{"line":74,"column":8},"end":{"line":74,"column":23}},"loc":{"start":{"line":74,"column":23},"end":{"line":89,"column":null}},"line":74},"16":{"name":"(anonymous_16)","decl":{"start":{"line":90,"column":8},"end":{"line":90,"column":24}},"loc":{"start":{"line":90,"column":24},"end":{"line":90,"column":null}},"line":90},"17":{"name":"fetchPhoto","decl":{"start":{"line":95,"column":16},"end":{"line":95,"column":null}},"loc":{"start":{"line":102,"column":2},"end":{"line":158,"column":null}},"line":102},"18":{"name":"(anonymous_18)","decl":{"start":{"line":118,"column":25},"end":{"line":118,"column":30}},"loc":{"start":{"line":118,"column":39},"end":{"line":120,"column":5}},"line":118},"19":{"name":"(anonymous_19)","decl":{"start":{"line":125,"column":23},"end":{"line":125,"column":null}},"loc":{"start":{"line":126,"column":4},"end":{"line":156,"column":null}},"line":126},"20":{"name":"(anonymous_20)","decl":{"start":{"line":127,"column":12},"end":{"line":127,"column":19}},"loc":{"start":{"line":127,"column":51},"end":{"line":149,"column":7}},"line":127},"21":{"name":"(anonymous_21)","decl":{"start":{"line":150,"column":7},"end":{"line":150,"column":19}},"loc":{"start":{"line":150,"column":19},"end":{"line":155,"column":7}},"line":150},"22":{"name":"(anonymous_22)","decl":{"start":{"line":156,"column":7},"end":{"line":156,"column":21}},"loc":{"start":{"line":156,"column":21},"end":{"line":156,"column":72}},"line":156},"23":{"name":"getAllThumbs","decl":{"start":{"line":160,"column":16},"end":{"line":160,"column":55}},"loc":{"start":{"line":160,"column":55},"end":{"line":166,"column":null}},"line":160}},"branchMap":{"0":{"loc":{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":22},"1":{"loc":{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},"type":"if","locations":[{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},{"start":{"line":33,"column":9},"end":{"line":35,"column":null}}],"line":31},"2":{"loc":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"type":"if","locations":[{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},{"start":{},"end":{}}],"line":49},"3":{"loc":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"type":"if","locations":[{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},{"start":{},"end":{}}],"line":56},"4":{"loc":{"start":{"line":70,"column":41},"end":{"line":70,"column":84}},"type":"default-arg","locations":[{"start":{"line":70,"column":56},"end":{"line":70,"column":84}}],"line":70},"5":{"loc":{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},"type":"if","locations":[{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},{"start":{},"end":{}}],"line":104},"6":{"loc":{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},"type":"if","locations":[{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},{"start":{},"end":{}}],"line":106},"7":{"loc":{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},"type":"if","locations":[{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},{"start":{},"end":{}}],"line":107},"8":{"loc":{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},"type":"if","locations":[{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},{"start":{},"end":{}}],"line":112},"9":{"loc":{"start":{"line":112,"column":6},"end":{"line":112,"column":63}},"type":"binary-expr","locations":[{"start":{"line":112,"column":6},"end":{"line":112,"column":17}},{"start":{"line":112,"column":17},"end":{"line":112,"column":63}}],"line":112},"10":{"loc":{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},"type":"if","locations":[{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},{"start":{},"end":{}}],"line":119},"11":{"loc":{"start":{"line":128,"column":25},"end":{"line":128,"column":null}},"type":"binary-expr","locations":[{"start":{"line":128,"column":25},"end":{"line":128,"column":42}},{"start":{"line":128,"column":42},"end":{"line":128,"column":null}}],"line":128},"12":{"loc":{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},"type":"if","locations":[{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},{"start":{},"end":{}}],"line":129},"13":{"loc":{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},"type":"if","locations":[{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},{"start":{},"end":{}}],"line":145},"14":{"loc":{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},"type":"if","locations":[{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},{"start":{},"end":{}}],"line":163}},"s":{"0":25,"1":25,"2":25,"3":25,"4":25,"5":25,"6":25,"7":23,"8":22,"9":22,"10":1,"11":1,"12":18,"13":18,"14":1,"15":17,"16":20,"17":5,"18":20,"19":15,"20":4,"21":15,"22":6,"23":5,"24":6,"25":6,"26":1,"27":7,"28":6,"29":7,"30":7,"31":1,"32":9,"33":3,"34":21,"35":21,"36":21,"37":21,"38":18,"39":18,"40":18,"41":18,"42":18,"43":18,"44":18,"45":18,"46":18,"47":18,"48":18,"49":18,"50":18,"51":1,"52":21,"53":3,"54":21,"55":29,"56":29,"57":1,"58":1,"59":28,"60":3,"61":2,"62":3,"63":25,"64":2,"65":2,"66":2,"67":2,"68":2,"69":2,"70":1,"71":1,"72":2,"73":23,"74":23,"75":23,"76":17,"77":17,"78":2,"79":2,"80":2,"81":2,"82":2,"83":15,"84":15,"85":15,"86":17,"87":17,"88":15,"89":14,"90":14,"91":1,"92":1,"93":1,"94":1,"95":18,"96":18,"97":1,"98":1,"99":2,"100":1,"101":1},"f":{"0":23,"1":1,"2":18,"3":20,"4":5,"5":15,"6":4,"7":6,"8":1,"9":7,"10":1,"11":9,"12":3,"13":21,"14":21,"15":18,"16":3,"17":29,"18":2,"19":23,"20":17,"21":1,"22":18,"23":1},"b":{"0":[22,1],"1":[1,17],"2":[5,1],"3":[6,1],"4":[21],"5":[1,28],"6":[3,25],"7":[2,1],"8":[2,23],"9":[25,25],"10":[1,1],"11":[17,2],"12":[2,15],"13":[14,1],"14":[1,1]},"meta":{"lastBranch":15,"lastFunction":24,"lastStatement":102,"seen":{"s:9:14:9:Infinity":0,"s:10:17:10:Infinity":1,"s:11:18:11:Infinity":2,"s:13:23:13:Infinity":3,"s:17:23:17:Infinity":4,"s:18:21:18:Infinity":5,"s:19:40:19:Infinity":6,"f:21:9:21:45":0,"b:22:2:25:Infinity:undefined:undefined:undefined:undefined":0,"s:22:2:25:Infinity":7,"s:23:4:23:Infinity":8,"s:24:4:24:Infinity":9,"s:26:2:26:Infinity":10,"f:26:13:26:21":1,"s:26:32:26:58":11,"f:29:9:29:36":2,"s:30:15:30:Infinity":12,"b:31:2:35:Infinity:33:9:35:Infinity":1,"s:31:2:35:Infinity":13,"s:32:4:32:Infinity":14,"s:34:4:34:Infinity":15,"f:38:9:38:16":3,"s:39:2:39:Infinity":16,"f:39:22:39:30":4,"s:39:36:39:45":17,"s:40:2:40:Infinity":18,"f:43:9:43:21":5,"s:44:2:44:Infinity":19,"f:44:27:44:35":6,"s:44:41:44:50":20,"s:45:2:45:Infinity":21,"f:48:16:48:30":7,"b:49:2:49:Infinity:undefined:undefined:undefined:undefined":2,"s:49:2:49:Infinity":22,"s:49:27:49:Infinity":23,"s:50:2:50:Infinity":24,"s:51:2:51:Infinity":25,"f:51:2:51:15":8,"s:51:17:51:48":26,"f:55:16:55:29":9,"b:56:2:56:Infinity:undefined:undefined:undefined:undefined":3,"s:56:2:56:Infinity":27,"s:56:32:56:Infinity":28,"s:57:2:57:Infinity":29,"s:58:2:58:Infinity":30,"f:58:2:58:15":10,"s:58:17:58:53":31,"f:61:16:61:26":11,"s:62:2:62:Infinity":32,"f:65:16:65:26":12,"s:66:2:66:Infinity":33,"f:70:16:70:28":13,"b:70:56:70:84":4,"s:71:2:92:Infinity":34,"f:71:13:71:21":14,"s:72:16:72:Infinity":35,"s:73:4:73:Infinity":36,"s:74:4:89:Infinity":37,"f:74:8:74:23":15,"s:75:6:88:Infinity":38,"s:76:23:76:Infinity":39,"s:77:8:77:Infinity":40,"s:78:8:78:Infinity":41,"s:79:20:79:Infinity":42,"s:80:18:80:Infinity":43,"s:81:14:81:Infinity":44,"s:82:14:82:Infinity":45,"s:83:8:83:Infinity":46,"s:84:8:84:Infinity":47,"s:85:8:85:Infinity":48,"s:86:8:86:Infinity":49,"s:87:8:87:Infinity":50,"s:88:16:88:30":51,"s:90:4:90:Infinity":52,"f:90:8:90:24":16,"s:90:24:90:Infinity":53,"s:91:4:91:Infinity":54,"f:95:16:95:Infinity":17,"s:103:17:103:Infinity":55,"b:104:2:104:Infinity:undefined:undefined:undefined:undefined":5,"s:104:2:104:Infinity":56,"s:104:16:104:36":57,"s:104:36:104:43":58,"b:106:2:109:Infinity:undefined:undefined:undefined:undefined":6,"s:106:2:109:Infinity":59,"b:107:4:107:Infinity:undefined:undefined:undefined:undefined":7,"s:107:4:107:Infinity":60,"s:107:18:107:Infinity":61,"s:108:4:108:Infinity":62,"b:112:2:122:Infinity:undefined:undefined:undefined:undefined":8,"s:112:2:122:Infinity":63,"b:112:6:112:17:112:17:112:63":9,"s:113:30:113:Infinity":64,"s:114:4:114:Infinity":65,"s:115:4:115:Infinity":66,"s:116:4:116:Infinity":67,"s:118:4:120:Infinity":68,"f:118:25:118:30":18,"b:119:6:119:Infinity:undefined:undefined:undefined:undefined":10,"s:119:6:119:Infinity":69,"s:119:19:119:47":70,"s:119:47:119:76":71,"s:121:4:121:Infinity":72,"s:124:2:124:Infinity":73,"s:125:2:157:Infinity":74,"f:125:23:125:Infinity":19,"s:126:4:156:Infinity":75,"f:127:12:127:19":20,"s:128:25:128:Infinity":76,"b:128:25:128:42:128:42:128:Infinity":11,"b:129:8:135:Infinity:undefined:undefined:undefined:undefined":12,"s:129:8:135:Infinity":77,"s:130:36:130:Infinity":78,"s:131:10:131:Infinity":79,"s:132:10:132:Infinity":80,"s:133:10:133:Infinity":81,"s:134:10:134:Infinity":82,"s:138:34:138:Infinity":83,"s:139:8:139:Infinity":84,"s:140:8:140:Infinity":85,"s:141:8:141:Infinity":86,"s:144:22:144:Infinity":87,"b:145:8:148:Infinity:undefined:undefined:undefined:undefined":13,"s:145:8:148:Infinity":88,"s:146:10:146:Infinity":89,"s:147:10:147:Infinity":90,"f:150:7:150:19":21,"s:151:34:151:Infinity":91,"s:152:8:152:Infinity":92,"s:153:8:153:Infinity":93,"s:154:8:154:Infinity":94,"f:156:7:156:21":22,"s:156:23:156:50":95,"s:156:50:156:71":96,"f:160:16:160:55":23,"s:161:36:161:Infinity":97,"s:162:2:164:Infinity":98,"b:163:4:163:Infinity:undefined:undefined:undefined:undefined":14,"s:163:4:163:Infinity":99,"s:163:24:163:Infinity":100,"s:165:2:165:Infinity":101},"fnNames":{}}} -,"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\store\\authStore.ts": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\store\\authStore.ts","statementMap":{"0":{"start":{"line":75,"column":19},"end":{"line":75,"column":null}},"1":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"2":{"start":{"line":83,"column":2},"end":{"line":87,"column":null}},"3":{"start":{"line":84,"column":4},"end":{"line":84,"column":null}},"4":{"start":{"line":86,"column":4},"end":{"line":86,"column":null}},"5":{"start":{"line":90,"column":13},"end":{"line":369,"column":null}},"6":{"start":{"line":92,"column":17},"end":{"line":348,"column":null}},"7":{"start":{"line":111,"column":4},"end":{"line":111,"column":null}},"8":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"9":{"start":{"line":113,"column":4},"end":{"line":136,"column":null}},"10":{"start":{"line":114,"column":19},"end":{"line":114,"column":null}},"11":{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},"12":{"start":{"line":116,"column":8},"end":{"line":116,"column":null}},"13":{"start":{"line":117,"column":8},"end":{"line":117,"column":null}},"14":{"start":{"line":119,"column":6},"end":{"line":124,"column":null}},"15":{"start":{"line":125,"column":6},"end":{"line":125,"column":null}},"16":{"start":{"line":126,"column":6},"end":{"line":126,"column":null}},"17":{"start":{"line":127,"column":6},"end":{"line":127,"column":null}},"18":{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},"19":{"start":{"line":129,"column":8},"end":{"line":129,"column":null}},"20":{"start":{"line":131,"column":6},"end":{"line":131,"column":null}},"21":{"start":{"line":133,"column":12},"end":{"line":133,"column":null}},"22":{"start":{"line":134,"column":6},"end":{"line":134,"column":null}},"23":{"start":{"line":135,"column":6},"end":{"line":135,"column":null}},"24":{"start":{"line":140,"column":4},"end":{"line":140,"column":null}},"25":{"start":{"line":141,"column":4},"end":{"line":141,"column":null}},"26":{"start":{"line":142,"column":4},"end":{"line":161,"column":null}},"27":{"start":{"line":143,"column":19},"end":{"line":143,"column":null}},"28":{"start":{"line":144,"column":6},"end":{"line":149,"column":null}},"29":{"start":{"line":150,"column":6},"end":{"line":150,"column":null}},"30":{"start":{"line":151,"column":6},"end":{"line":151,"column":null}},"31":{"start":{"line":152,"column":6},"end":{"line":152,"column":null}},"32":{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},"33":{"start":{"line":154,"column":8},"end":{"line":154,"column":null}},"34":{"start":{"line":156,"column":6},"end":{"line":156,"column":null}},"35":{"start":{"line":158,"column":12},"end":{"line":158,"column":null}},"36":{"start":{"line":159,"column":6},"end":{"line":159,"column":null}},"37":{"start":{"line":160,"column":6},"end":{"line":160,"column":null}},"38":{"start":{"line":165,"column":4},"end":{"line":165,"column":null}},"39":{"start":{"line":166,"column":4},"end":{"line":166,"column":null}},"40":{"start":{"line":167,"column":4},"end":{"line":184,"column":null}},"41":{"start":{"line":168,"column":19},"end":{"line":168,"column":null}},"42":{"start":{"line":169,"column":6},"end":{"line":174,"column":null}},"43":{"start":{"line":175,"column":6},"end":{"line":175,"column":null}},"44":{"start":{"line":176,"column":6},"end":{"line":176,"column":null}},"45":{"start":{"line":177,"column":6},"end":{"line":177,"column":null}},"46":{"start":{"line":178,"column":6},"end":{"line":178,"column":null}},"47":{"start":{"line":179,"column":6},"end":{"line":179,"column":null}},"48":{"start":{"line":181,"column":12},"end":{"line":181,"column":null}},"49":{"start":{"line":182,"column":6},"end":{"line":182,"column":null}},"50":{"start":{"line":183,"column":6},"end":{"line":183,"column":null}},"51":{"start":{"line":189,"column":4},"end":{"line":189,"column":null}},"52":{"start":{"line":190,"column":4},"end":{"line":190,"column":null}},"53":{"start":{"line":192,"column":4},"end":{"line":192,"column":null}},"54":{"start":{"line":194,"column":4},"end":{"line":194,"column":null}},"55":{"start":{"line":195,"column":4},"end":{"line":195,"column":null}},"56":{"start":{"line":198,"column":4},"end":{"line":198,"column":null}},"57":{"start":{"line":201,"column":4},"end":{"line":201,"column":null}},"58":{"start":{"line":203,"column":4},"end":{"line":203,"column":null}},"59":{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},"60":{"start":{"line":206,"column":6},"end":{"line":209,"column":null}},"61":{"start":{"line":212,"column":4},"end":{"line":212,"column":null}},"62":{"start":{"line":214,"column":4},"end":{"line":219,"column":null}},"63":{"start":{"line":223,"column":16},"end":{"line":223,"column":null}},"64":{"start":{"line":224,"column":19},"end":{"line":224,"column":null}},"65":{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},"66":{"start":{"line":225,"column":17},"end":{"line":225,"column":null}},"67":{"start":{"line":226,"column":4},"end":{"line":261,"column":null}},"68":{"start":{"line":227,"column":19},"end":{"line":227,"column":null}},"69":{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},"70":{"start":{"line":228,"column":32},"end":{"line":228,"column":null}},"71":{"start":{"line":229,"column":6},"end":{"line":234,"column":null}},"72":{"start":{"line":235,"column":6},"end":{"line":235,"column":null}},"73":{"start":{"line":236,"column":6},"end":{"line":236,"column":null}},"74":{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},"75":{"start":{"line":238,"column":32},"end":{"line":238,"column":null}},"76":{"start":{"line":239,"column":21},"end":{"line":241,"column":null}},"77":{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},"78":{"start":{"line":244,"column":8},"end":{"line":249,"column":null}},"79":{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},"80":{"start":{"line":253,"column":8},"end":{"line":253,"column":null}},"81":{"start":{"line":259,"column":8},"end":{"line":259,"column":null}},"82":{"start":{"line":265,"column":4},"end":{"line":273,"column":null}},"83":{"start":{"line":266,"column":6},"end":{"line":266,"column":null}},"84":{"start":{"line":267,"column":6},"end":{"line":270,"column":null}},"85":{"start":{"line":267,"column":22},"end":{"line":270,"column":8}},"86":{"start":{"line":272,"column":6},"end":{"line":272,"column":null}},"87":{"start":{"line":277,"column":4},"end":{"line":285,"column":null}},"88":{"start":{"line":278,"column":19},"end":{"line":278,"column":null}},"89":{"start":{"line":279,"column":6},"end":{"line":279,"column":null}},"90":{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},"91":{"start":{"line":281,"column":8},"end":{"line":281,"column":null}},"92":{"start":{"line":284,"column":6},"end":{"line":284,"column":null}},"93":{"start":{"line":289,"column":4},"end":{"line":294,"column":null}},"94":{"start":{"line":290,"column":19},"end":{"line":290,"column":null}},"95":{"start":{"line":291,"column":6},"end":{"line":291,"column":null}},"96":{"start":{"line":293,"column":6},"end":{"line":293,"column":null}},"97":{"start":{"line":298,"column":21},"end":{"line":298,"column":null}},"98":{"start":{"line":299,"column":4},"end":{"line":299,"column":null}},"99":{"start":{"line":300,"column":17},"end":{"line":300,"column":null}},"100":{"start":{"line":301,"column":4},"end":{"line":301,"column":null}},"101":{"start":{"line":301,"column":20},"end":{"line":301,"column":97}},"102":{"start":{"line":302,"column":4},"end":{"line":302,"column":null}},"103":{"start":{"line":306,"column":4},"end":{"line":306,"column":null}},"104":{"start":{"line":307,"column":4},"end":{"line":307,"column":null}},"105":{"start":{"line":307,"column":20},"end":{"line":307,"column":86}},"106":{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},"107":{"start":{"line":311,"column":13},"end":{"line":311,"column":null}},"108":{"start":{"line":312,"column":9},"end":{"line":312,"column":null}},"109":{"start":{"line":313,"column":4},"end":{"line":313,"column":null}},"110":{"start":{"line":316,"column":32},"end":{"line":316,"column":null}},"111":{"start":{"line":317,"column":37},"end":{"line":317,"column":null}},"112":{"start":{"line":318,"column":34},"end":{"line":318,"column":null}},"113":{"start":{"line":319,"column":35},"end":{"line":319,"column":null}},"114":{"start":{"line":320,"column":37},"end":{"line":320,"column":null}},"115":{"start":{"line":321,"column":38},"end":{"line":321,"column":null}},"116":{"start":{"line":322,"column":45},"end":{"line":322,"column":null}},"117":{"start":{"line":323,"column":44},"end":{"line":323,"column":null}},"118":{"start":{"line":324,"column":50},"end":{"line":324,"column":null}},"119":{"start":{"line":325,"column":45},"end":{"line":325,"column":null}},"120":{"start":{"line":328,"column":4},"end":{"line":328,"column":null}},"121":{"start":{"line":329,"column":4},"end":{"line":329,"column":null}},"122":{"start":{"line":330,"column":4},"end":{"line":346,"column":null}},"123":{"start":{"line":331,"column":19},"end":{"line":331,"column":null}},"124":{"start":{"line":332,"column":6},"end":{"line":338,"column":null}},"125":{"start":{"line":339,"column":6},"end":{"line":339,"column":null}},"126":{"start":{"line":340,"column":6},"end":{"line":340,"column":null}},"127":{"start":{"line":341,"column":6},"end":{"line":341,"column":null}},"128":{"start":{"line":343,"column":12},"end":{"line":343,"column":null}},"129":{"start":{"line":344,"column":6},"end":{"line":344,"column":null}},"130":{"start":{"line":345,"column":6},"end":{"line":345,"column":null}},"131":{"start":{"line":356,"column":28},"end":{"line":367,"column":null}}},"fnMap":{"0":{"name":"onAuthSuccess","decl":{"start":{"line":81,"column":15},"end":{"line":81,"column":29}},"loc":{"start":{"line":81,"column":60},"end":{"line":88,"column":null}},"line":81},"1":{"name":"(anonymous_1)","decl":{"start":{"line":91,"column":2},"end":{"line":91,"column":null}},"loc":{"start":{"line":92,"column":17},"end":{"line":348,"column":null}},"line":92},"2":{"name":"(anonymous_2)","decl":{"start":{"line":110,"column":9},"end":{"line":110,"column":16}},"loc":{"start":{"line":110,"column":74},"end":{"line":137,"column":null}},"line":110},"3":{"name":"(anonymous_3)","decl":{"start":{"line":139,"column":20},"end":{"line":139,"column":27}},"loc":{"start":{"line":139,"column":84},"end":{"line":162,"column":null}},"line":139},"4":{"name":"(anonymous_4)","decl":{"start":{"line":164,"column":12},"end":{"line":164,"column":19}},"loc":{"start":{"line":164,"column":96},"end":{"line":185,"column":null}},"line":164},"5":{"name":"(anonymous_5)","decl":{"start":{"line":187,"column":10},"end":{"line":187,"column":22}},"loc":{"start":{"line":187,"column":22},"end":{"line":220,"column":null}},"line":187},"6":{"name":"(anonymous_6)","decl":{"start":{"line":203,"column":80},"end":{"line":203,"column":92}},"loc":{"start":{"line":203,"column":92},"end":{"line":203,"column":94}},"line":203},"7":{"name":"(anonymous_7)","decl":{"start":{"line":207,"column":34},"end":{"line":207,"column":46}},"loc":{"start":{"line":207,"column":46},"end":{"line":207,"column":48}},"line":207},"8":{"name":"(anonymous_8)","decl":{"start":{"line":208,"column":38},"end":{"line":208,"column":50}},"loc":{"start":{"line":208,"column":50},"end":{"line":208,"column":52}},"line":208},"9":{"name":"(anonymous_9)","decl":{"start":{"line":222,"column":12},"end":{"line":222,"column":19}},"loc":{"start":{"line":222,"column":51},"end":{"line":262,"column":null}},"line":222},"10":{"name":"(anonymous_10)","decl":{"start":{"line":264,"column":17},"end":{"line":264,"column":24}},"loc":{"start":{"line":264,"column":47},"end":{"line":274,"column":null}},"line":264},"11":{"name":"(anonymous_11)","decl":{"start":{"line":267,"column":6},"end":{"line":267,"column":11}},"loc":{"start":{"line":267,"column":22},"end":{"line":270,"column":8}},"line":267},"12":{"name":"(anonymous_12)","decl":{"start":{"line":276,"column":17},"end":{"line":276,"column":24}},"loc":{"start":{"line":276,"column":64},"end":{"line":286,"column":null}},"line":276},"13":{"name":"(anonymous_13)","decl":{"start":{"line":288,"column":17},"end":{"line":288,"column":24}},"loc":{"start":{"line":288,"column":55},"end":{"line":295,"column":null}},"line":288},"14":{"name":"(anonymous_14)","decl":{"start":{"line":297,"column":16},"end":{"line":297,"column":23}},"loc":{"start":{"line":297,"column":38},"end":{"line":303,"column":null}},"line":297},"15":{"name":"(anonymous_15)","decl":{"start":{"line":301,"column":4},"end":{"line":301,"column":9}},"loc":{"start":{"line":301,"column":20},"end":{"line":301,"column":97}},"line":301},"16":{"name":"(anonymous_16)","decl":{"start":{"line":305,"column":16},"end":{"line":305,"column":28}},"loc":{"start":{"line":305,"column":28},"end":{"line":308,"column":null}},"line":305},"17":{"name":"(anonymous_17)","decl":{"start":{"line":307,"column":4},"end":{"line":307,"column":9}},"loc":{"start":{"line":307,"column":20},"end":{"line":307,"column":86}},"line":307},"18":{"name":"(anonymous_18)","decl":{"start":{"line":310,"column":2},"end":{"line":310,"column":16}},"loc":{"start":{"line":310,"column":33},"end":{"line":314,"column":null}},"line":310},"19":{"name":"(anonymous_19)","decl":{"start":{"line":316,"column":2},"end":{"line":316,"column":15}},"loc":{"start":{"line":316,"column":32},"end":{"line":316,"column":null}},"line":316},"20":{"name":"(anonymous_20)","decl":{"start":{"line":317,"column":2},"end":{"line":317,"column":20}},"loc":{"start":{"line":317,"column":37},"end":{"line":317,"column":null}},"line":317},"21":{"name":"(anonymous_21)","decl":{"start":{"line":318,"column":2},"end":{"line":318,"column":18}},"loc":{"start":{"line":318,"column":34},"end":{"line":318,"column":null}},"line":318},"22":{"name":"(anonymous_22)","decl":{"start":{"line":319,"column":2},"end":{"line":319,"column":18}},"loc":{"start":{"line":319,"column":35},"end":{"line":319,"column":null}},"line":319},"23":{"name":"(anonymous_23)","decl":{"start":{"line":320,"column":2},"end":{"line":320,"column":22}},"loc":{"start":{"line":320,"column":37},"end":{"line":320,"column":null}},"line":320},"24":{"name":"(anonymous_24)","decl":{"start":{"line":321,"column":2},"end":{"line":321,"column":21}},"loc":{"start":{"line":321,"column":38},"end":{"line":321,"column":null}},"line":321},"25":{"name":"(anonymous_25)","decl":{"start":{"line":322,"column":2},"end":{"line":322,"column":28}},"loc":{"start":{"line":322,"column":45},"end":{"line":322,"column":null}},"line":322},"26":{"name":"(anonymous_26)","decl":{"start":{"line":323,"column":2},"end":{"line":323,"column":27}},"loc":{"start":{"line":323,"column":44},"end":{"line":323,"column":null}},"line":323},"27":{"name":"(anonymous_27)","decl":{"start":{"line":324,"column":2},"end":{"line":324,"column":33}},"loc":{"start":{"line":324,"column":50},"end":{"line":324,"column":null}},"line":324},"28":{"name":"(anonymous_28)","decl":{"start":{"line":325,"column":2},"end":{"line":325,"column":28}},"loc":{"start":{"line":325,"column":45},"end":{"line":325,"column":null}},"line":325},"29":{"name":"(anonymous_29)","decl":{"start":{"line":327,"column":13},"end":{"line":327,"column":25}},"loc":{"start":{"line":327,"column":25},"end":{"line":347,"column":null}},"line":327},"30":{"name":"(anonymous_30)","decl":{"start":{"line":356,"column":4},"end":{"line":356,"column":17}},"loc":{"start":{"line":356,"column":28},"end":{"line":367,"column":null}},"line":356}},"branchMap":{"0":{"loc":{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},"type":"if","locations":[{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},{"start":{},"end":{}}],"line":115},"1":{"loc":{"start":{"line":115,"column":10},"end":{"line":115,"column":47}},"type":"binary-expr","locations":[{"start":{"line":115,"column":10},"end":{"line":115,"column":31}},{"start":{"line":115,"column":31},"end":{"line":115,"column":47}}],"line":115},"2":{"loc":{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},"type":"if","locations":[{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},{"start":{},"end":{}}],"line":128},"3":{"loc":{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},"type":"if","locations":[{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},{"start":{},"end":{}}],"line":153},"4":{"loc":{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},"type":"if","locations":[{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},{"start":{},"end":{}}],"line":205},"5":{"loc":{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},"type":"if","locations":[{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},{"start":{},"end":{}}],"line":225},"6":{"loc":{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},"type":"if","locations":[{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},{"start":{},"end":{}}],"line":228},"7":{"loc":{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},"type":"if","locations":[{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},{"start":{},"end":{}}],"line":238},"8":{"loc":{"start":{"line":239,"column":21},"end":{"line":241,"column":null}},"type":"cond-expr","locations":[{"start":{"line":240,"column":11},"end":{"line":240,"column":null}},{"start":{"line":241,"column":10},"end":{"line":241,"column":null}}],"line":239},"9":{"loc":{"start":{"line":239,"column":21},"end":{"line":239,"column":null}},"type":"binary-expr","locations":[{"start":{"line":239,"column":21},"end":{"line":239,"column":28}},{"start":{"line":239,"column":28},"end":{"line":239,"column":55}},{"start":{"line":239,"column":55},"end":{"line":239,"column":null}}],"line":239},"10":{"loc":{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},"type":"if","locations":[{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},{"start":{"line":250,"column":13},"end":{"line":260,"column":null}}],"line":242},"11":{"loc":{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},"type":"if","locations":[{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},{"start":{"line":254,"column":13},"end":{"line":260,"column":null}}],"line":250},"12":{"loc":{"start":{"line":250,"column":17},"end":{"line":250,"column":96}},"type":"binary-expr","locations":[{"start":{"line":250,"column":17},"end":{"line":250,"column":41}},{"start":{"line":250,"column":41},"end":{"line":250,"column":77}},{"start":{"line":250,"column":77},"end":{"line":250,"column":96}}],"line":250},"13":{"loc":{"start":{"line":268,"column":14},"end":{"line":268,"column":null}},"type":"cond-expr","locations":[{"start":{"line":268,"column":27},"end":{"line":268,"column":74}},{"start":{"line":268,"column":74},"end":{"line":268,"column":null}}],"line":268},"14":{"loc":{"start":{"line":268,"column":58},"end":{"line":268,"column":70}},"type":"binary-expr","locations":[{"start":{"line":268,"column":58},"end":{"line":268,"column":65}},{"start":{"line":268,"column":65},"end":{"line":268,"column":70}}],"line":268},"15":{"loc":{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},"type":"if","locations":[{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},{"start":{},"end":{}}],"line":280},"16":{"loc":{"start":{"line":301,"column":28},"end":{"line":301,"column":95}},"type":"cond-expr","locations":[{"start":{"line":301,"column":41},"end":{"line":301,"column":90}},{"start":{"line":301,"column":90},"end":{"line":301,"column":95}}],"line":301},"17":{"loc":{"start":{"line":307,"column":28},"end":{"line":307,"column":84}},"type":"cond-expr","locations":[{"start":{"line":307,"column":41},"end":{"line":307,"column":79}},{"start":{"line":307,"column":79},"end":{"line":307,"column":84}}],"line":307},"18":{"loc":{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},"type":"if","locations":[{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},{"start":{"line":312,"column":9},"end":{"line":312,"column":null}}],"line":311},"19":{"loc":{"start":{"line":358,"column":12},"end":{"line":366,"column":null}},"type":"cond-expr","locations":[{"start":{"line":358,"column":25},"end":{"line":366,"column":10}},{"start":{"line":366,"column":10},"end":{"line":366,"column":null}}],"line":358}},"s":{"0":1,"1":12,"2":12,"3":12,"4":1,"5":1,"6":1,"7":8,"8":8,"9":8,"10":8,"11":7,"12":1,"13":1,"14":6,"15":6,"16":6,"17":6,"18":6,"19":5,"20":6,"21":1,"22":1,"23":1,"24":3,"25":3,"26":3,"27":3,"28":2,"29":2,"30":2,"31":2,"32":2,"33":1,"34":2,"35":1,"36":1,"37":1,"38":2,"39":2,"40":2,"41":2,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":3,"52":3,"53":3,"54":3,"55":3,"56":3,"57":3,"58":3,"59":3,"60":1,"61":3,"62":3,"63":10,"64":10,"65":10,"66":9,"67":10,"68":10,"69":3,"70":1,"71":2,"72":2,"73":2,"74":7,"75":1,"76":6,"77":7,"78":2,"79":4,"80":1,"81":3,"82":4,"83":4,"84":3,"85":3,"86":1,"87":4,"88":4,"89":3,"90":3,"91":2,"92":1,"93":2,"94":2,"95":1,"96":1,"97":2,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2,"106":2,"107":1,"108":1,"109":2,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"118":1,"119":1,"120":2,"121":2,"122":2,"123":2,"124":1,"125":1,"126":1,"127":1,"128":1,"129":1,"130":1,"131":140},"f":{"0":12,"1":1,"2":8,"3":3,"4":2,"5":3,"6":1,"7":1,"8":1,"9":10,"10":4,"11":3,"12":4,"13":2,"14":2,"15":2,"16":2,"17":2,"18":2,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":2,"30":140},"b":{"0":[1,6],"1":[7,1],"2":[5,1],"3":[1,1],"4":[1,2],"5":[9,1],"6":[1,2],"7":[1,6],"8":[4,2],"9":[6,6,6],"10":[2,4],"11":[1,3],"12":[4,2,2],"13":[2,1],"14":[2,1],"15":[2,1],"16":[1,1],"17":[1,1],"18":[1,1],"19":[50,90]},"meta":{"lastBranch":20,"lastFunction":31,"lastStatement":132,"seen":{"s:75:19:75:Infinity":0,"f:81:15:81:29":0,"s:82:2:82:Infinity":1,"s:83:2:87:Infinity":2,"s:84:4:84:Infinity":3,"s:86:4:86:Infinity":4,"s:90:13:369:Infinity":5,"f:91:2:91:Infinity":1,"s:92:17:348:Infinity":6,"f:110:9:110:16":2,"s:111:4:111:Infinity":7,"s:112:4:112:Infinity":8,"s:113:4:136:Infinity":9,"s:114:19:114:Infinity":10,"b:115:6:118:Infinity:undefined:undefined:undefined:undefined":0,"s:115:6:118:Infinity":11,"b:115:10:115:31:115:31:115:47":1,"s:116:8:116:Infinity":12,"s:117:8:117:Infinity":13,"s:119:6:124:Infinity":14,"s:125:6:125:Infinity":15,"s:126:6:126:Infinity":16,"s:127:6:127:Infinity":17,"b:128:6:130:Infinity:undefined:undefined:undefined:undefined":2,"s:128:6:130:Infinity":18,"s:129:8:129:Infinity":19,"s:131:6:131:Infinity":20,"s:133:12:133:Infinity":21,"s:134:6:134:Infinity":22,"s:135:6:135:Infinity":23,"f:139:20:139:27":3,"s:140:4:140:Infinity":24,"s:141:4:141:Infinity":25,"s:142:4:161:Infinity":26,"s:143:19:143:Infinity":27,"s:144:6:149:Infinity":28,"s:150:6:150:Infinity":29,"s:151:6:151:Infinity":30,"s:152:6:152:Infinity":31,"b:153:6:155:Infinity:undefined:undefined:undefined:undefined":3,"s:153:6:155:Infinity":32,"s:154:8:154:Infinity":33,"s:156:6:156:Infinity":34,"s:158:12:158:Infinity":35,"s:159:6:159:Infinity":36,"s:160:6:160:Infinity":37,"f:164:12:164:19":4,"s:165:4:165:Infinity":38,"s:166:4:166:Infinity":39,"s:167:4:184:Infinity":40,"s:168:19:168:Infinity":41,"s:169:6:174:Infinity":42,"s:175:6:175:Infinity":43,"s:176:6:176:Infinity":44,"s:177:6:177:Infinity":45,"s:178:6:178:Infinity":46,"s:179:6:179:Infinity":47,"s:181:12:181:Infinity":48,"s:182:6:182:Infinity":49,"s:183:6:183:Infinity":50,"f:187:10:187:22":5,"s:189:4:189:Infinity":51,"s:190:4:190:Infinity":52,"s:192:4:192:Infinity":53,"s:194:4:194:Infinity":54,"s:195:4:195:Infinity":55,"s:198:4:198:Infinity":56,"s:201:4:201:Infinity":57,"s:203:4:203:Infinity":58,"f:203:80:203:92":6,"b:205:4:210:Infinity:undefined:undefined:undefined:undefined":4,"s:205:4:210:Infinity":59,"s:206:6:209:Infinity":60,"f:207:34:207:46":7,"f:208:38:208:50":8,"s:212:4:212:Infinity":61,"s:214:4:219:Infinity":62,"f:222:12:222:19":9,"s:223:16:223:Infinity":63,"s:224:19:224:Infinity":64,"b:225:4:225:Infinity:undefined:undefined:undefined:undefined":5,"s:225:4:225:Infinity":65,"s:225:17:225:Infinity":66,"s:226:4:261:Infinity":67,"s:227:19:227:Infinity":68,"b:228:6:228:Infinity:undefined:undefined:undefined:undefined":6,"s:228:6:228:Infinity":69,"s:228:32:228:Infinity":70,"s:229:6:234:Infinity":71,"s:235:6:235:Infinity":72,"s:236:6:236:Infinity":73,"b:238:6:238:Infinity:undefined:undefined:undefined:undefined":7,"s:238:6:238:Infinity":74,"s:238:32:238:Infinity":75,"s:239:21:241:Infinity":76,"b:240:11:240:Infinity:241:10:241:Infinity":8,"b:239:21:239:28:239:28:239:55:239:55:239:Infinity":9,"b:242:6:260:Infinity:250:13:260:Infinity":10,"s:242:6:260:Infinity":77,"s:244:8:249:Infinity":78,"b:250:13:260:Infinity:254:13:260:Infinity":11,"s:250:13:260:Infinity":79,"b:250:17:250:41:250:41:250:77:250:77:250:96":12,"s:253:8:253:Infinity":80,"s:259:8:259:Infinity":81,"f:264:17:264:24":10,"s:265:4:273:Infinity":82,"s:266:6:266:Infinity":83,"s:267:6:270:Infinity":84,"f:267:6:267:11":11,"s:267:22:270:8":85,"b:268:27:268:74:268:74:268:Infinity":13,"b:268:58:268:65:268:65:268:70":14,"s:272:6:272:Infinity":86,"f:276:17:276:24":12,"s:277:4:285:Infinity":87,"s:278:19:278:Infinity":88,"s:279:6:279:Infinity":89,"b:280:6:282:Infinity:undefined:undefined:undefined:undefined":15,"s:280:6:282:Infinity":90,"s:281:8:281:Infinity":91,"s:284:6:284:Infinity":92,"f:288:17:288:24":13,"s:289:4:294:Infinity":93,"s:290:19:290:Infinity":94,"s:291:6:291:Infinity":95,"s:293:6:293:Infinity":96,"f:297:16:297:23":14,"s:298:21:298:Infinity":97,"s:299:4:299:Infinity":98,"s:300:17:300:Infinity":99,"s:301:4:301:Infinity":100,"f:301:4:301:9":15,"s:301:20:301:97":101,"b:301:41:301:90:301:90:301:95":16,"s:302:4:302:Infinity":102,"f:305:16:305:28":16,"s:306:4:306:Infinity":103,"s:307:4:307:Infinity":104,"f:307:4:307:9":17,"s:307:20:307:86":105,"b:307:41:307:79:307:79:307:84":17,"f:310:2:310:16":18,"b:311:4:312:Infinity:312:9:312:Infinity":18,"s:311:4:312:Infinity":106,"s:311:13:311:Infinity":107,"s:312:9:312:Infinity":108,"s:313:4:313:Infinity":109,"f:316:2:316:15":19,"s:316:32:316:Infinity":110,"f:317:2:317:20":20,"s:317:37:317:Infinity":111,"f:318:2:318:18":21,"s:318:34:318:Infinity":112,"f:319:2:319:18":22,"s:319:35:319:Infinity":113,"f:320:2:320:22":23,"s:320:37:320:Infinity":114,"f:321:2:321:21":24,"s:321:38:321:Infinity":115,"f:322:2:322:28":25,"s:322:45:322:Infinity":116,"f:323:2:323:27":26,"s:323:44:323:Infinity":117,"f:324:2:324:33":27,"s:324:50:324:Infinity":118,"f:325:2:325:28":28,"s:325:45:325:Infinity":119,"f:327:13:327:25":29,"s:328:4:328:Infinity":120,"s:329:4:329:Infinity":121,"s:330:4:346:Infinity":122,"s:331:19:331:Infinity":123,"s:332:6:338:Infinity":124,"s:339:6:339:Infinity":125,"s:340:6:340:Infinity":126,"s:341:6:341:Infinity":127,"s:343:12:343:Infinity":128,"s:344:6:344:Infinity":129,"s:345:6:345:Infinity":130,"f:356:4:356:17":30,"s:356:28:367:Infinity":131,"b:358:25:366:10:366:10:366:Infinity":19},"fnNames":{}}} -} diff --git a/client/coverage-jsheet/coverage-final.json b/client/coverage-jsheet/coverage-final.json deleted file mode 100644 index c34c70058..000000000 --- a/client/coverage-jsheet/coverage-final.json +++ /dev/null @@ -1,2 +0,0 @@ -{"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\mobile\\screens\\journey\\MJourneyEntrySheet.tsx": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\mobile\\screens\\journey\\MJourneyEntrySheet.tsx","statementMap":{"0":{"start":{"line":16,"column":18},"end":{"line":16,"column":null}},"1":{"start":{"line":17,"column":18},"end":{"line":17,"column":null}},"2":{"start":{"line":46,"column":10},"end":{"line":46,"column":null}},"3":{"start":{"line":47,"column":8},"end":{"line":47,"column":null}},"4":{"start":{"line":49,"column":16},"end":{"line":49,"column":null}},"5":{"start":{"line":50,"column":16},"end":{"line":50,"column":null}},"6":{"start":{"line":51,"column":20},"end":{"line":51,"column":null}},"7":{"start":{"line":52,"column":20},"end":{"line":52,"column":null}},"8":{"start":{"line":53,"column":23},"end":{"line":53,"column":null}},"9":{"start":{"line":54,"column":22},"end":{"line":54,"column":null}},"10":{"start":{"line":55,"column":22},"end":{"line":55,"column":null}},"11":{"start":{"line":56,"column":24},"end":{"line":56,"column":null}},"12":{"start":{"line":57,"column":26},"end":{"line":57,"column":null}},"13":{"start":{"line":58,"column":30},"end":{"line":58,"column":null}},"14":{"start":{"line":59,"column":8},"end":{"line":59,"column":null}},"15":{"start":{"line":60,"column":15},"end":{"line":60,"column":null}},"16":{"start":{"line":61,"column":18},"end":{"line":61,"column":null}},"17":{"start":{"line":62,"column":15},"end":{"line":62,"column":null}},"18":{"start":{"line":63,"column":15},"end":{"line":63,"column":null}},"19":{"start":{"line":64,"column":15},"end":{"line":64,"column":null}},"20":{"start":{"line":65,"column":19},"end":{"line":65,"column":null}},"21":{"start":{"line":66,"column":17},"end":{"line":66,"column":null}},"22":{"start":{"line":67,"column":23},"end":{"line":67,"column":null}},"23":{"start":{"line":68,"column":25},"end":{"line":68,"column":null}},"24":{"start":{"line":69,"column":26},"end":{"line":69,"column":null}},"25":{"start":{"line":70,"column":17},"end":{"line":70,"column":null}},"26":{"start":{"line":71,"column":22},"end":{"line":71,"column":null}},"27":{"start":{"line":72,"column":19},"end":{"line":72,"column":null}},"28":{"start":{"line":73,"column":24},"end":{"line":73,"column":null}},"29":{"start":{"line":74,"column":25},"end":{"line":74,"column":null}},"30":{"start":{"line":75,"column":8},"end":{"line":75,"column":null}},"31":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"32":{"start":{"line":78,"column":2},"end":{"line":113,"column":null}},"33":{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},"34":{"start":{"line":79,"column":95},"end":{"line":79,"column":null}},"35":{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},"36":{"start":{"line":81,"column":6},"end":{"line":81,"column":null}},"37":{"start":{"line":82,"column":6},"end":{"line":82,"column":null}},"38":{"start":{"line":85,"column":17},"end":{"line":85,"column":null}},"39":{"start":{"line":86,"column":4},"end":{"line":86,"column":null}},"40":{"start":{"line":87,"column":4},"end":{"line":110,"column":null}},"41":{"start":{"line":88,"column":18},"end":{"line":88,"column":null}},"42":{"start":{"line":89,"column":18},"end":{"line":89,"column":null}},"43":{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},"44":{"start":{"line":90,"column":19},"end":{"line":90,"column":null}},"45":{"start":{"line":91,"column":6},"end":{"line":91,"column":null}},"46":{"start":{"line":92,"column":6},"end":{"line":92,"column":null}},"47":{"start":{"line":94,"column":43},"end":{"line":97,"column":null}},"48":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},"49":{"start":{"line":98,"column":19},"end":{"line":98,"column":null}},"50":{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},"51":{"start":{"line":100,"column":8},"end":{"line":100,"column":null}},"52":{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},"53":{"start":{"line":103,"column":8},"end":{"line":103,"column":null}},"54":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"55":{"start":{"line":105,"column":6},"end":{"line":105,"column":null}},"56":{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},"57":{"start":{"line":107,"column":19},"end":{"line":107,"column":null}},"58":{"start":{"line":108,"column":6},"end":{"line":108,"column":null}},"59":{"start":{"line":109,"column":6},"end":{"line":109,"column":null}},"60":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"61":{"start":{"line":112,"column":19},"end":{"line":112,"column":34}},"62":{"start":{"line":116,"column":4},"end":{"line":127,"column":null}},"63":{"start":{"line":129,"column":8},"end":{"line":132,"column":null}},"64":{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},"65":{"start":{"line":130,"column":108},"end":{"line":130,"column":null}},"66":{"start":{"line":131,"column":4},"end":{"line":131,"column":null}},"67":{"start":{"line":134,"column":21},"end":{"line":179,"column":null}},"68":{"start":{"line":135,"column":4},"end":{"line":135,"column":null}},"69":{"start":{"line":136,"column":4},"end":{"line":178,"column":null}},"70":{"start":{"line":137,"column":22},"end":{"line":152,"column":null}},"71":{"start":{"line":147,"column":33},"end":{"line":147,"column":43}},"72":{"start":{"line":148,"column":44},"end":{"line":148,"column":52}},"73":{"start":{"line":148,"column":78},"end":{"line":148,"column":86}},"74":{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},"75":{"start":{"line":154,"column":25},"end":{"line":154,"column":null}},"76":{"start":{"line":155,"column":8},"end":{"line":155,"column":null}},"77":{"start":{"line":156,"column":8},"end":{"line":168,"column":null}},"78":{"start":{"line":157,"column":29},"end":{"line":159,"column":null}},"79":{"start":{"line":158,"column":29},"end":{"line":158,"column":null}},"80":{"start":{"line":160,"column":10},"end":{"line":160,"column":null}},"81":{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},"82":{"start":{"line":162,"column":12},"end":{"line":162,"column":null}},"83":{"start":{"line":165,"column":10},"end":{"line":165,"column":null}},"84":{"start":{"line":167,"column":10},"end":{"line":167,"column":null}},"85":{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},"86":{"start":{"line":171,"column":8},"end":{"line":173,"column":null}},"87":{"start":{"line":172,"column":10},"end":{"line":172,"column":null}},"88":{"start":{"line":172,"column":16},"end":{"line":172,"column":61}},"89":{"start":{"line":175,"column":6},"end":{"line":175,"column":null}},"90":{"start":{"line":177,"column":6},"end":{"line":177,"column":null}},"91":{"start":{"line":181,"column":27},"end":{"line":186,"column":null}},"92":{"start":{"line":182,"column":18},"end":{"line":182,"column":null}},"93":{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},"94":{"start":{"line":183,"column":24},"end":{"line":183,"column":null}},"95":{"start":{"line":184,"column":23},"end":{"line":184,"column":null}},"96":{"start":{"line":185,"column":4},"end":{"line":185,"column":null}},"97":{"start":{"line":185,"column":28},"end":{"line":185,"column":52}},"98":{"start":{"line":188,"column":8},"end":{"line":206,"column":null}},"99":{"start":{"line":189,"column":4},"end":{"line":189,"column":null}},"100":{"start":{"line":190,"column":4},"end":{"line":190,"column":null}},"101":{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},"102":{"start":{"line":191,"column":34},"end":{"line":191,"column":null}},"103":{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},"104":{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},"105":{"start":{"line":194,"column":6},"end":{"line":194,"column":null}},"106":{"start":{"line":196,"column":4},"end":{"line":205,"column":null}},"107":{"start":{"line":197,"column":6},"end":{"line":204,"column":null}},"108":{"start":{"line":198,"column":20},"end":{"line":198,"column":null}},"109":{"start":{"line":199,"column":8},"end":{"line":201,"column":null}},"110":{"start":{"line":199,"column":150},"end":{"line":201,"column":10}},"111":{"start":{"line":203,"column":8},"end":{"line":203,"column":null}},"112":{"start":{"line":208,"column":8},"end":{"line":213,"column":null}},"113":{"start":{"line":209,"column":18},"end":{"line":209,"column":null}},"114":{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},"115":{"start":{"line":210,"column":16},"end":{"line":210,"column":null}},"116":{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},"117":{"start":{"line":211,"column":31},"end":{"line":211,"column":null}},"118":{"start":{"line":211,"column":47},"end":{"line":211,"column":63}},"119":{"start":{"line":212,"column":4},"end":{"line":212,"column":null}},"120":{"start":{"line":215,"column":33},"end":{"line":215,"column":null}},"121":{"start":{"line":215,"column":60},"end":{"line":215,"column":93}},"122":{"start":{"line":215,"column":78},"end":{"line":215,"column":92}},"123":{"start":{"line":217,"column":18},"end":{"line":217,"column":null}},"124":{"start":{"line":218,"column":21},"end":{"line":218,"column":null}},"125":{"start":{"line":220,"column":2},"end":{"line":665,"column":null}},"126":{"start":{"line":243,"column":27},"end":{"line":243,"column":null}},"127":{"start":{"line":251,"column":149},"end":{"line":251,"column":191}},"128":{"start":{"line":252,"column":134},"end":{"line":252,"column":176}},"129":{"start":{"line":256,"column":16},"end":{"line":256,"column":null}},"130":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"131":{"start":{"line":274,"column":96},"end":{"line":274,"column":98}},"132":{"start":{"line":291,"column":20},"end":{"line":309,"column":null}},"133":{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},"134":{"start":{"line":298,"column":26},"end":{"line":301,"column":null}},"135":{"start":{"line":299,"column":43},"end":{"line":299,"column":null}},"136":{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},"137":{"start":{"line":300,"column":40},"end":{"line":300,"column":null}},"138":{"start":{"line":300,"column":58},"end":{"line":300,"column":75}},"139":{"start":{"line":303,"column":26},"end":{"line":303,"column":null}},"140":{"start":{"line":303,"column":52},"end":{"line":303,"column":68}},"141":{"start":{"line":304,"column":26},"end":{"line":304,"column":null}},"142":{"start":{"line":304,"column":44},"end":{"line":304,"column":57}},"143":{"start":{"line":325,"column":14},"end":{"line":361,"column":null}},"144":{"start":{"line":331,"column":22},"end":{"line":337,"column":null}},"145":{"start":{"line":332,"column":37},"end":{"line":332,"column":null}},"146":{"start":{"line":333,"column":40},"end":{"line":333,"column":null}},"147":{"start":{"line":334,"column":24},"end":{"line":334,"column":null}},"148":{"start":{"line":335,"column":24},"end":{"line":335,"column":null}},"149":{"start":{"line":335,"column":50},"end":{"line":335,"column":115}},"150":{"start":{"line":336,"column":24},"end":{"line":336,"column":null}},"151":{"start":{"line":348,"column":22},"end":{"line":348,"column":null}},"152":{"start":{"line":348,"column":40},"end":{"line":348,"column":71}},"153":{"start":{"line":348,"column":57},"end":{"line":348,"column":70}},"154":{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},"155":{"start":{"line":350,"column":24},"end":{"line":350,"column":null}},"156":{"start":{"line":350,"column":30},"end":{"line":350,"column":75}},"157":{"start":{"line":352,"column":24},"end":{"line":352,"column":null}},"158":{"start":{"line":352,"column":50},"end":{"line":352,"column":80}},"159":{"start":{"line":352,"column":68},"end":{"line":352,"column":79}},"160":{"start":{"line":364,"column":14},"end":{"line":374,"column":null}},"161":{"start":{"line":368,"column":33},"end":{"line":368,"column":null}},"162":{"start":{"line":368,"column":57},"end":{"line":368,"column":87}},"163":{"start":{"line":368,"column":79},"end":{"line":368,"column":86}},"164":{"start":{"line":390,"column":29},"end":{"line":390,"column":null}},"165":{"start":{"line":407,"column":18},"end":{"line":421,"column":null}},"166":{"start":{"line":412,"column":52},"end":{"line":412,"column":63}},"167":{"start":{"line":412,"column":63},"end":{"line":412,"column":89}},"168":{"start":{"line":412,"column":89},"end":{"line":412,"column":103}},"169":{"start":{"line":417,"column":59},"end":{"line":417,"column":100}},"170":{"start":{"line":417,"column":89},"end":{"line":417,"column":96}},"171":{"start":{"line":426,"column":35},"end":{"line":426,"column":null}},"172":{"start":{"line":440,"column":18},"end":{"line":454,"column":null}},"173":{"start":{"line":445,"column":52},"end":{"line":445,"column":63}},"174":{"start":{"line":445,"column":63},"end":{"line":445,"column":89}},"175":{"start":{"line":445,"column":89},"end":{"line":445,"column":103}},"176":{"start":{"line":450,"column":59},"end":{"line":450,"column":100}},"177":{"start":{"line":450,"column":89},"end":{"line":450,"column":96}},"178":{"start":{"line":459,"column":35},"end":{"line":459,"column":null}},"179":{"start":{"line":481,"column":31},"end":{"line":481,"column":null}},"180":{"start":{"line":493,"column":31},"end":{"line":493,"column":null}},"181":{"start":{"line":507,"column":29},"end":{"line":507,"column":null}},"182":{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},"183":{"start":{"line":508,"column":63},"end":{"line":508,"column":92}},"184":{"start":{"line":517,"column":16},"end":{"line":535,"column":null}},"185":{"start":{"line":521,"column":20},"end":{"line":521,"column":null}},"186":{"start":{"line":522,"column":20},"end":{"line":522,"column":null}},"187":{"start":{"line":523,"column":20},"end":{"line":523,"column":null}},"188":{"start":{"line":524,"column":20},"end":{"line":524,"column":null}},"189":{"start":{"line":525,"column":20},"end":{"line":525,"column":null}},"190":{"start":{"line":526,"column":20},"end":{"line":526,"column":null}},"191":{"start":{"line":548,"column":29},"end":{"line":548,"column":null}},"192":{"start":{"line":549,"column":14},"end":{"line":562,"column":null}},"193":{"start":{"line":554,"column":33},"end":{"line":554,"column":null}},"194":{"start":{"line":572,"column":27},"end":{"line":572,"column":null}},"195":{"start":{"line":573,"column":12},"end":{"line":587,"column":null}},"196":{"start":{"line":578,"column":31},"end":{"line":578,"column":null}},"197":{"start":{"line":598,"column":16},"end":{"line":605,"column":null}},"198":{"start":{"line":601,"column":57},"end":{"line":601,"column":103}},"199":{"start":{"line":601,"column":73},"end":{"line":601,"column":100}},"200":{"start":{"line":601,"column":90},"end":{"line":601,"column":99}},"201":{"start":{"line":610,"column":33},"end":{"line":610,"column":null}},"202":{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},"203":{"start":{"line":613,"column":22},"end":{"line":613,"column":null}},"204":{"start":{"line":614,"column":22},"end":{"line":614,"column":null}},"205":{"start":{"line":641,"column":27},"end":{"line":641,"column":null}}},"fnMap":{"0":{"name":"MJourneyEntrySheet","decl":{"start":{"line":43,"column":24},"end":{"line":43,"column":43}},"loc":{"start":{"line":45,"column":28},"end":{"line":667,"column":null}},"line":45},"1":{"name":"(anonymous_1)","decl":{"start":{"line":78,"column":2},"end":{"line":78,"column":18}},"loc":{"start":{"line":78,"column":18},"end":{"line":113,"column":5}},"line":78},"2":{"name":"(anonymous_2)","decl":{"start":{"line":87,"column":45},"end":{"line":87,"column":51}},"loc":{"start":{"line":87,"column":63},"end":{"line":106,"column":7}},"line":87},"3":{"name":"(anonymous_3)","decl":{"start":{"line":103,"column":8},"end":{"line":103,"column":19}},"loc":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"line":103},"4":{"name":"(anonymous_4)","decl":{"start":{"line":106,"column":4},"end":{"line":106,"column":7}},"loc":{"start":{"line":106,"column":16},"end":{"line":110,"column":7}},"line":106},"5":{"name":"(anonymous_5)","decl":{"start":{"line":112,"column":4},"end":{"line":112,"column":17}},"loc":{"start":{"line":112,"column":17},"end":{"line":112,"column":null}},"line":112},"6":{"name":"(anonymous_6)","decl":{"start":{"line":129,"column":8},"end":{"line":129,"column":28}},"loc":{"start":{"line":129,"column":28},"end":{"line":132,"column":null}},"line":129},"7":{"name":"(anonymous_7)","decl":{"start":{"line":134,"column":21},"end":{"line":134,"column":33}},"loc":{"start":{"line":134,"column":33},"end":{"line":179,"column":null}},"line":134},"8":{"name":"(anonymous_8)","decl":{"start":{"line":147,"column":19},"end":{"line":147,"column":26}},"loc":{"start":{"line":147,"column":33},"end":{"line":147,"column":43}},"line":147},"9":{"name":"(anonymous_9)","decl":{"start":{"line":148,"column":32},"end":{"line":148,"column":39}},"loc":{"start":{"line":148,"column":44},"end":{"line":148,"column":52}},"line":148},"10":{"name":"(anonymous_10)","decl":{"start":{"line":148,"column":66},"end":{"line":148,"column":73}},"loc":{"start":{"line":148,"column":78},"end":{"line":148,"column":86}},"line":148},"11":{"name":"(anonymous_11)","decl":{"start":{"line":158,"column":12},"end":{"line":158,"column":24}},"loc":{"start":{"line":158,"column":29},"end":{"line":158,"column":null}},"line":158},"12":{"name":"(anonymous_12)","decl":{"start":{"line":181,"column":27},"end":{"line":181,"column":34}},"loc":{"start":{"line":181,"column":77},"end":{"line":186,"column":null}},"line":181},"13":{"name":"(anonymous_13)","decl":{"start":{"line":185,"column":4},"end":{"line":185,"column":20}},"loc":{"start":{"line":185,"column":28},"end":{"line":185,"column":52}},"line":185},"14":{"name":"(anonymous_14)","decl":{"start":{"line":188,"column":8},"end":{"line":188,"column":26}},"loc":{"start":{"line":188,"column":44},"end":{"line":206,"column":null}},"line":188},"15":{"name":"(anonymous_15)","decl":{"start":{"line":196,"column":42},"end":{"line":196,"column":54}},"loc":{"start":{"line":196,"column":54},"end":{"line":205,"column":7}},"line":196},"16":{"name":"(anonymous_16)","decl":{"start":{"line":199,"column":58},"end":{"line":199,"column":63}},"loc":{"start":{"line":199,"column":150},"end":{"line":201,"column":10}},"line":199},"17":{"name":"(anonymous_17)","decl":{"start":{"line":208,"column":8},"end":{"line":208,"column":23}},"loc":{"start":{"line":208,"column":23},"end":{"line":213,"column":null}},"line":208},"18":{"name":"(anonymous_18)","decl":{"start":{"line":211,"column":31},"end":{"line":211,"column":39}},"loc":{"start":{"line":211,"column":47},"end":{"line":211,"column":63}},"line":211},"19":{"name":"(anonymous_19)","decl":{"start":{"line":215,"column":47},"end":{"line":215,"column":54}},"loc":{"start":{"line":215,"column":60},"end":{"line":215,"column":93}},"line":215},"20":{"name":"(anonymous_20)","decl":{"start":{"line":215,"column":68},"end":{"line":215,"column":73}},"loc":{"start":{"line":215,"column":78},"end":{"line":215,"column":92}},"line":215},"21":{"name":"(anonymous_21)","decl":{"start":{"line":243,"column":12},"end":{"line":243,"column":22}},"loc":{"start":{"line":243,"column":27},"end":{"line":243,"column":null}},"line":243},"22":{"name":"(anonymous_22)","decl":{"start":{"line":251,"column":133},"end":{"line":251,"column":142}},"loc":{"start":{"line":251,"column":147},"end":{"line":251,"column":195}},"line":251},"23":{"name":"(anonymous_23)","decl":{"start":{"line":252,"column":118},"end":{"line":252,"column":127}},"loc":{"start":{"line":252,"column":132},"end":{"line":252,"column":180}},"line":252},"24":{"name":"(anonymous_24)","decl":{"start":{"line":256,"column":16},"end":{"line":256,"column":32}},"loc":{"start":{"line":256,"column":16},"end":{"line":256,"column":null}},"line":256},"25":{"name":"(anonymous_25)","decl":{"start":{"line":274,"column":16},"end":{"line":274,"column":31}},"loc":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"line":274},"26":{"name":"(anonymous_26)","decl":{"start":{"line":274,"column":72},"end":{"line":274,"column":91}},"loc":{"start":{"line":274,"column":96},"end":{"line":274,"column":98}},"line":274},"27":{"name":"(anonymous_27)","decl":{"start":{"line":290,"column":42},"end":{"line":290,"column":46}},"loc":{"start":{"line":291,"column":20},"end":{"line":309,"column":null}},"line":291},"28":{"name":"(anonymous_28)","decl":{"start":{"line":296,"column":31},"end":{"line":296,"column":43}},"loc":{"start":{"line":296,"column":43},"end":{"line":306,"column":null}},"line":296},"29":{"name":"(anonymous_29)","decl":{"start":{"line":300,"column":40},"end":{"line":300,"column":50}},"loc":{"start":{"line":300,"column":58},"end":{"line":300,"column":75}},"line":300},"30":{"name":"(anonymous_30)","decl":{"start":{"line":303,"column":26},"end":{"line":303,"column":44}},"loc":{"start":{"line":303,"column":52},"end":{"line":303,"column":68}},"line":303},"31":{"name":"(anonymous_31)","decl":{"start":{"line":304,"column":26},"end":{"line":304,"column":36}},"loc":{"start":{"line":304,"column":44},"end":{"line":304,"column":57}},"line":304},"32":{"name":"(anonymous_32)","decl":{"start":{"line":324,"column":20},"end":{"line":324,"column":25}},"loc":{"start":{"line":325,"column":14},"end":{"line":361,"column":null}},"line":325},"33":{"name":"(anonymous_33)","decl":{"start":{"line":330,"column":20},"end":{"line":330,"column":35}},"loc":{"start":{"line":330,"column":35},"end":{"line":338,"column":null}},"line":330},"34":{"name":"(anonymous_34)","decl":{"start":{"line":331,"column":22},"end":{"line":331,"column":32}},"loc":{"start":{"line":331,"column":40},"end":{"line":337,"column":23}},"line":331},"35":{"name":"(anonymous_35)","decl":{"start":{"line":335,"column":29},"end":{"line":335,"column":38}},"loc":{"start":{"line":335,"column":48},"end":{"line":335,"column":116}},"line":335},"36":{"name":"(anonymous_36)","decl":{"start":{"line":335,"column":99},"end":{"line":335,"column":111}},"loc":{"start":{"line":335,"column":111},"end":{"line":335,"column":113}},"line":335},"37":{"name":"(anonymous_37)","decl":{"start":{"line":347,"column":29},"end":{"line":347,"column":41}},"loc":{"start":{"line":347,"column":41},"end":{"line":354,"column":null}},"line":347},"38":{"name":"(anonymous_38)","decl":{"start":{"line":348,"column":22},"end":{"line":348,"column":32}},"loc":{"start":{"line":348,"column":40},"end":{"line":348,"column":71}},"line":348},"39":{"name":"(anonymous_39)","decl":{"start":{"line":348,"column":45},"end":{"line":348,"column":52}},"loc":{"start":{"line":348,"column":57},"end":{"line":348,"column":70}},"line":348},"40":{"name":"(anonymous_40)","decl":{"start":{"line":352,"column":24},"end":{"line":352,"column":42}},"loc":{"start":{"line":352,"column":50},"end":{"line":352,"column":80}},"line":352},"41":{"name":"(anonymous_41)","decl":{"start":{"line":352,"column":55},"end":{"line":352,"column":62}},"loc":{"start":{"line":352,"column":68},"end":{"line":352,"column":79}},"line":352},"42":{"name":"(anonymous_42)","decl":{"start":{"line":363,"column":26},"end":{"line":363,"column":31}},"loc":{"start":{"line":364,"column":14},"end":{"line":374,"column":null}},"line":364},"43":{"name":"(anonymous_43)","decl":{"start":{"line":368,"column":18},"end":{"line":368,"column":33}},"loc":{"start":{"line":368,"column":33},"end":{"line":368,"column":null}},"line":368},"44":{"name":"(anonymous_44)","decl":{"start":{"line":368,"column":33},"end":{"line":368,"column":49}},"loc":{"start":{"line":368,"column":57},"end":{"line":368,"column":87}},"line":368},"45":{"name":"(anonymous_45)","decl":{"start":{"line":368,"column":62},"end":{"line":368,"column":70}},"loc":{"start":{"line":368,"column":79},"end":{"line":368,"column":86}},"line":368},"46":{"name":"(anonymous_46)","decl":{"start":{"line":390,"column":14},"end":{"line":390,"column":24}},"loc":{"start":{"line":390,"column":29},"end":{"line":390,"column":null}},"line":390},"47":{"name":"(anonymous_47)","decl":{"start":{"line":406,"column":22},"end":{"line":406,"column":27}},"loc":{"start":{"line":407,"column":18},"end":{"line":421,"column":null}},"line":407},"48":{"name":"(anonymous_48)","decl":{"start":{"line":412,"column":22},"end":{"line":412,"column":32}},"loc":{"start":{"line":412,"column":37},"end":{"line":412,"column":null}},"line":412},"49":{"name":"(anonymous_49)","decl":{"start":{"line":417,"column":44},"end":{"line":417,"column":59}},"loc":{"start":{"line":417,"column":59},"end":{"line":417,"column":100}},"line":417},"50":{"name":"(anonymous_50)","decl":{"start":{"line":417,"column":72},"end":{"line":417,"column":80}},"loc":{"start":{"line":417,"column":89},"end":{"line":417,"column":96}},"line":417},"51":{"name":"(anonymous_51)","decl":{"start":{"line":426,"column":20},"end":{"line":426,"column":35}},"loc":{"start":{"line":426,"column":35},"end":{"line":426,"column":null}},"line":426},"52":{"name":"(anonymous_52)","decl":{"start":{"line":439,"column":22},"end":{"line":439,"column":27}},"loc":{"start":{"line":440,"column":18},"end":{"line":454,"column":null}},"line":440},"53":{"name":"(anonymous_53)","decl":{"start":{"line":445,"column":22},"end":{"line":445,"column":32}},"loc":{"start":{"line":445,"column":37},"end":{"line":445,"column":null}},"line":445},"54":{"name":"(anonymous_54)","decl":{"start":{"line":450,"column":44},"end":{"line":450,"column":59}},"loc":{"start":{"line":450,"column":59},"end":{"line":450,"column":100}},"line":450},"55":{"name":"(anonymous_55)","decl":{"start":{"line":450,"column":72},"end":{"line":450,"column":80}},"loc":{"start":{"line":450,"column":89},"end":{"line":450,"column":96}},"line":450},"56":{"name":"(anonymous_56)","decl":{"start":{"line":459,"column":20},"end":{"line":459,"column":35}},"loc":{"start":{"line":459,"column":35},"end":{"line":459,"column":null}},"line":459},"57":{"name":"(anonymous_57)","decl":{"start":{"line":481,"column":16},"end":{"line":481,"column":26}},"loc":{"start":{"line":481,"column":31},"end":{"line":481,"column":null}},"line":481},"58":{"name":"(anonymous_58)","decl":{"start":{"line":493,"column":16},"end":{"line":493,"column":26}},"loc":{"start":{"line":493,"column":31},"end":{"line":493,"column":null}},"line":493},"59":{"name":"(anonymous_59)","decl":{"start":{"line":507,"column":14},"end":{"line":507,"column":24}},"loc":{"start":{"line":507,"column":29},"end":{"line":507,"column":null}},"line":507},"60":{"name":"(anonymous_60)","decl":{"start":{"line":508,"column":14},"end":{"line":508,"column":29}},"loc":{"start":{"line":508,"column":29},"end":{"line":508,"column":null}},"line":508},"61":{"name":"(anonymous_61)","decl":{"start":{"line":516,"column":31},"end":{"line":516,"column":36}},"loc":{"start":{"line":517,"column":16},"end":{"line":535,"column":null}},"line":517},"62":{"name":"(anonymous_62)","decl":{"start":{"line":520,"column":18},"end":{"line":520,"column":33}},"loc":{"start":{"line":520,"column":33},"end":{"line":527,"column":null}},"line":520},"63":{"name":"(anonymous_63)","decl":{"start":{"line":547,"column":26},"end":{"line":547,"column":30}},"loc":{"start":{"line":547,"column":35},"end":{"line":564,"column":13}},"line":547},"64":{"name":"(anonymous_64)","decl":{"start":{"line":554,"column":18},"end":{"line":554,"column":33}},"loc":{"start":{"line":554,"column":33},"end":{"line":554,"column":null}},"line":554},"65":{"name":"(anonymous_65)","decl":{"start":{"line":571,"column":27},"end":{"line":571,"column":31}},"loc":{"start":{"line":571,"column":36},"end":{"line":589,"column":11}},"line":571},"66":{"name":"(anonymous_66)","decl":{"start":{"line":578,"column":16},"end":{"line":578,"column":31}},"loc":{"start":{"line":578,"column":31},"end":{"line":578,"column":null}},"line":578},"67":{"name":"(anonymous_67)","decl":{"start":{"line":597,"column":20},"end":{"line":597,"column":24}},"loc":{"start":{"line":598,"column":16},"end":{"line":605,"column":null}},"line":598},"68":{"name":"(anonymous_68)","decl":{"start":{"line":601,"column":42},"end":{"line":601,"column":57}},"loc":{"start":{"line":601,"column":57},"end":{"line":601,"column":103}},"line":601},"69":{"name":"(anonymous_69)","decl":{"start":{"line":601,"column":57},"end":{"line":601,"column":65}},"loc":{"start":{"line":601,"column":73},"end":{"line":601,"column":100}},"line":601},"70":{"name":"(anonymous_70)","decl":{"start":{"line":601,"column":78},"end":{"line":601,"column":85}},"loc":{"start":{"line":601,"column":90},"end":{"line":601,"column":99}},"line":601},"71":{"name":"(anonymous_71)","decl":{"start":{"line":610,"column":18},"end":{"line":610,"column":28}},"loc":{"start":{"line":610,"column":33},"end":{"line":610,"column":null}},"line":610},"72":{"name":"(anonymous_72)","decl":{"start":{"line":611,"column":18},"end":{"line":611,"column":29}},"loc":{"start":{"line":611,"column":34},"end":{"line":616,"column":null}},"line":611},"73":{"name":"(anonymous_73)","decl":{"start":{"line":641,"column":12},"end":{"line":641,"column":27}},"loc":{"start":{"line":641,"column":27},"end":{"line":641,"column":null}},"line":641}},"branchMap":{"0":{"loc":{"start":{"line":44,"column":24},"end":{"line":44,"column":46}},"type":"default-arg","locations":[{"start":{"line":44,"column":39},"end":{"line":44,"column":46}}],"line":44},"1":{"loc":{"start":{"line":44,"column":46},"end":{"line":44,"column":64}},"type":"default-arg","locations":[{"start":{"line":44,"column":57},"end":{"line":44,"column":64}}],"line":44},"2":{"loc":{"start":{"line":49,"column":37},"end":{"line":49,"column":54}},"type":"binary-expr","locations":[{"start":{"line":49,"column":37},"end":{"line":49,"column":52}},{"start":{"line":49,"column":52},"end":{"line":49,"column":54}}],"line":49},"3":{"loc":{"start":{"line":50,"column":37},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":50,"column":37},"end":{"line":50,"column":52}},{"start":{"line":50,"column":52},"end":{"line":50,"column":54}}],"line":50},"4":{"loc":{"start":{"line":51,"column":45},"end":{"line":51,"column":103}},"type":"binary-expr","locations":[{"start":{"line":51,"column":45},"end":{"line":51,"column":65}},{"start":{"line":51,"column":65},"end":{"line":51,"column":103}}],"line":51},"5":{"loc":{"start":{"line":52,"column":45},"end":{"line":52,"column":80}},"type":"binary-expr","locations":[{"start":{"line":52,"column":45},"end":{"line":52,"column":78}},{"start":{"line":52,"column":78},"end":{"line":52,"column":80}}],"line":52},"6":{"loc":{"start":{"line":53,"column":51},"end":{"line":53,"column":76}},"type":"binary-expr","locations":[{"start":{"line":53,"column":51},"end":{"line":53,"column":74}},{"start":{"line":53,"column":74},"end":{"line":53,"column":76}}],"line":53},"7":{"loc":{"start":{"line":54,"column":64},"end":{"line":54,"column":90}},"type":"binary-expr","locations":[{"start":{"line":54,"column":64},"end":{"line":54,"column":86}},{"start":{"line":54,"column":86},"end":{"line":54,"column":90}}],"line":54},"8":{"loc":{"start":{"line":55,"column":64},"end":{"line":55,"column":90}},"type":"binary-expr","locations":[{"start":{"line":55,"column":64},"end":{"line":55,"column":86}},{"start":{"line":55,"column":86},"end":{"line":55,"column":90}}],"line":55},"9":{"loc":{"start":{"line":60,"column":35},"end":{"line":60,"column":51}},"type":"binary-expr","locations":[{"start":{"line":60,"column":35},"end":{"line":60,"column":49}},{"start":{"line":60,"column":49},"end":{"line":60,"column":51}}],"line":60},"10":{"loc":{"start":{"line":61,"column":41},"end":{"line":61,"column":60}},"type":"binary-expr","locations":[{"start":{"line":61,"column":41},"end":{"line":61,"column":58}},{"start":{"line":61,"column":58},"end":{"line":61,"column":60}}],"line":61},"11":{"loc":{"start":{"line":62,"column":45},"end":{"line":62,"column":72}},"type":"binary-expr","locations":[{"start":{"line":62,"column":45},"end":{"line":62,"column":70}},{"start":{"line":62,"column":70},"end":{"line":62,"column":72}}],"line":62},"12":{"loc":{"start":{"line":63,"column":45},"end":{"line":63,"column":72}},"type":"binary-expr","locations":[{"start":{"line":63,"column":45},"end":{"line":63,"column":70}},{"start":{"line":63,"column":70},"end":{"line":63,"column":72}}],"line":63},"13":{"loc":{"start":{"line":64,"column":45},"end":{"line":64,"column":61}},"type":"binary-expr","locations":[{"start":{"line":64,"column":45},"end":{"line":64,"column":59}},{"start":{"line":64,"column":59},"end":{"line":64,"column":61}}],"line":64},"14":{"loc":{"start":{"line":66,"column":72},"end":{"line":66,"column":90}},"type":"binary-expr","locations":[{"start":{"line":66,"column":72},"end":{"line":66,"column":88}},{"start":{"line":66,"column":88},"end":{"line":66,"column":90}}],"line":66},"15":{"loc":{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},"type":"if","locations":[{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},{"start":{},"end":{}}],"line":79},"16":{"loc":{"start":{"line":79,"column":8},"end":{"line":79,"column":95}},"type":"binary-expr","locations":[{"start":{"line":79,"column":8},"end":{"line":79,"column":25}},{"start":{"line":79,"column":25},"end":{"line":79,"column":37}},{"start":{"line":79,"column":37},"end":{"line":79,"column":67}},{"start":{"line":79,"column":67},"end":{"line":79,"column":95}}],"line":79},"17":{"loc":{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},"type":"if","locations":[{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},{"start":{},"end":{}}],"line":80},"18":{"loc":{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},"type":"if","locations":[{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},{"start":{},"end":{}}],"line":90},"19":{"loc":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},"type":"if","locations":[{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},{"start":{},"end":{}}],"line":98},"20":{"loc":{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},"type":"if","locations":[{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},{"start":{},"end":{}}],"line":99},"21":{"loc":{"start":{"line":100,"column":24},"end":{"line":100,"column":81}},"type":"binary-expr","locations":[{"start":{"line":100,"column":24},"end":{"line":100,"column":50}},{"start":{"line":100,"column":50},"end":{"line":100,"column":79}},{"start":{"line":100,"column":79},"end":{"line":100,"column":81}}],"line":100},"22":{"loc":{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},"type":"if","locations":[{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},{"start":{},"end":{}}],"line":102},"23":{"loc":{"start":{"line":102,"column":10},"end":{"line":102,"column":78}},"type":"binary-expr","locations":[{"start":{"line":102,"column":10},"end":{"line":102,"column":50}},{"start":{"line":102,"column":50},"end":{"line":102,"column":78}}],"line":102},"24":{"loc":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"type":"binary-expr","locations":[{"start":{"line":103,"column":30},"end":{"line":103,"column":41}},{"start":{"line":103,"column":30},"end":{"line":103,"column":122}}],"line":103},"25":{"loc":{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},"type":"if","locations":[{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},{"start":{},"end":{}}],"line":107},"26":{"loc":{"start":{"line":108,"column":23},"end":{"line":108,"column":57}},"type":"binary-expr","locations":[{"start":{"line":108,"column":23},"end":{"line":108,"column":40}},{"start":{"line":108,"column":40},"end":{"line":108,"column":57}}],"line":108},"27":{"loc":{"start":{"line":116,"column":4},"end":{"line":127,"column":null}},"type":"binary-expr","locations":[{"start":{"line":116,"column":4},"end":{"line":116,"column":null}},{"start":{"line":117,"column":4},"end":{"line":117,"column":null}},{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},{"start":{"line":119,"column":4},"end":{"line":119,"column":null}},{"start":{"line":120,"column":4},"end":{"line":120,"column":null}},{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},{"start":{"line":122,"column":4},"end":{"line":122,"column":null}},{"start":{"line":123,"column":4},"end":{"line":123,"column":null}},{"start":{"line":124,"column":4},"end":{"line":124,"column":null}},{"start":{"line":125,"column":4},"end":{"line":125,"column":null}},{"start":{"line":126,"column":4},"end":{"line":126,"column":null}},{"start":{"line":127,"column":4},"end":{"line":127,"column":null}}],"line":116},"28":{"loc":{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},"type":"if","locations":[{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},{"start":{},"end":{}}],"line":130},"29":{"loc":{"start":{"line":130,"column":8},"end":{"line":130,"column":108}},"type":"binary-expr","locations":[{"start":{"line":130,"column":8},"end":{"line":130,"column":24}},{"start":{"line":130,"column":24},"end":{"line":130,"column":37}},{"start":{"line":130,"column":37},"end":{"line":130,"column":48}},{"start":{"line":130,"column":48},"end":{"line":130,"column":108}}],"line":130},"30":{"loc":{"start":{"line":138,"column":15},"end":{"line":138,"column":null}},"type":"binary-expr","locations":[{"start":{"line":138,"column":15},"end":{"line":138,"column":24}},{"start":{"line":138,"column":24},"end":{"line":138,"column":null}}],"line":138},"31":{"loc":{"start":{"line":139,"column":15},"end":{"line":139,"column":null}},"type":"binary-expr","locations":[{"start":{"line":139,"column":15},"end":{"line":139,"column":24}},{"start":{"line":139,"column":24},"end":{"line":139,"column":null}}],"line":139},"32":{"loc":{"start":{"line":141,"column":20},"end":{"line":141,"column":null}},"type":"binary-expr","locations":[{"start":{"line":141,"column":20},"end":{"line":141,"column":33}},{"start":{"line":141,"column":33},"end":{"line":141,"column":null}}],"line":141},"33":{"loc":{"start":{"line":142,"column":23},"end":{"line":142,"column":null}},"type":"binary-expr","locations":[{"start":{"line":142,"column":23},"end":{"line":142,"column":39}},{"start":{"line":142,"column":39},"end":{"line":142,"column":null}}],"line":142},"34":{"loc":{"start":{"line":145,"column":14},"end":{"line":145,"column":null}},"type":"binary-expr","locations":[{"start":{"line":145,"column":14},"end":{"line":145,"column":22}},{"start":{"line":145,"column":22},"end":{"line":145,"column":null}}],"line":145},"35":{"loc":{"start":{"line":146,"column":17},"end":{"line":146,"column":null}},"type":"binary-expr","locations":[{"start":{"line":146,"column":17},"end":{"line":146,"column":28}},{"start":{"line":146,"column":28},"end":{"line":146,"column":null}}],"line":146},"36":{"loc":{"start":{"line":149,"column":14},"end":{"line":151,"column":null}},"type":"cond-expr","locations":[{"start":{"line":150,"column":12},"end":{"line":150,"column":null}},{"start":{"line":151,"column":12},"end":{"line":151,"column":null}}],"line":149},"37":{"loc":{"start":{"line":149,"column":14},"end":{"line":149,"column":null}},"type":"binary-expr","locations":[{"start":{"line":149,"column":14},"end":{"line":149,"column":44}},{"start":{"line":149,"column":44},"end":{"line":149,"column":60}},{"start":{"line":149,"column":60},"end":{"line":149,"column":87}},{"start":{"line":149,"column":87},"end":{"line":149,"column":null}}],"line":149},"38":{"loc":{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},"type":"if","locations":[{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},{"start":{},"end":{}}],"line":153},"39":{"loc":{"start":{"line":153,"column":10},"end":{"line":153,"column":46}},"type":"binary-expr","locations":[{"start":{"line":153,"column":10},"end":{"line":153,"column":37}},{"start":{"line":153,"column":37},"end":{"line":153,"column":46}}],"line":153},"40":{"loc":{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},"type":"if","locations":[{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},{"start":{},"end":{}}],"line":161},"41":{"loc":{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},"type":"if","locations":[{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},{"start":{},"end":{}}],"line":170},"42":{"loc":{"start":{"line":170,"column":10},"end":{"line":170,"column":48}},"type":"binary-expr","locations":[{"start":{"line":170,"column":10},"end":{"line":170,"column":39}},{"start":{"line":170,"column":39},"end":{"line":170,"column":48}}],"line":170},"43":{"loc":{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},"type":"if","locations":[{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},{"start":{},"end":{}}],"line":183},"44":{"loc":{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},"type":"if","locations":[{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},{"start":{},"end":{}}],"line":191},"45":{"loc":{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},"type":"if","locations":[{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},{"start":{},"end":{}}],"line":192},"46":{"loc":{"start":{"line":199,"column":28},"end":{"line":199,"column":46}},"type":"binary-expr","locations":[{"start":{"line":199,"column":28},"end":{"line":199,"column":42}},{"start":{"line":199,"column":42},"end":{"line":199,"column":46}}],"line":199},"47":{"loc":{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},"type":"if","locations":[{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},{"start":{},"end":{}}],"line":210},"48":{"loc":{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},"type":"if","locations":[{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},{"start":{},"end":{}}],"line":211},"49":{"loc":{"start":{"line":226,"column":17},"end":{"line":226,"column":null}},"type":"cond-expr","locations":[{"start":{"line":226,"column":34},"end":{"line":226,"column":65}},{"start":{"line":226,"column":65},"end":{"line":226,"column":null}}],"line":226},"50":{"loc":{"start":{"line":230,"column":11},"end":{"line":230,"column":null}},"type":"cond-expr","locations":[{"start":{"line":230,"column":28},"end":{"line":230,"column":59}},{"start":{"line":230,"column":59},"end":{"line":230,"column":null}}],"line":230},"51":{"loc":{"start":{"line":238,"column":9},"end":{"line":246,"column":null}},"type":"binary-expr","locations":[{"start":{"line":238,"column":9},"end":{"line":238,"column":26}},{"start":{"line":238,"column":26},"end":{"line":246,"column":null}}],"line":238},"52":{"loc":{"start":{"line":238,"column":26},"end":{"line":246,"column":null}},"type":"cond-expr","locations":[{"start":{"line":239,"column":10},"end":{"line":241,"column":11}},{"start":{"line":241,"column":10},"end":{"line":246,"column":null}}],"line":238},"53":{"loc":{"start":{"line":239,"column":73},"end":{"line":239,"column":124}},"type":"binary-expr","locations":[{"start":{"line":239,"column":73},"end":{"line":239,"column":82}},{"start":{"line":239,"column":82},"end":{"line":239,"column":124}}],"line":239},"54":{"loc":{"start":{"line":249,"column":9},"end":{"line":319,"column":null}},"type":"binary-expr","locations":[{"start":{"line":249,"column":9},"end":{"line":249,"column":null}},{"start":{"line":250,"column":10},"end":{"line":319,"column":null}}],"line":249},"55":{"loc":{"start":{"line":256,"column":32},"end":{"line":256,"column":67}},"type":"cond-expr","locations":[{"start":{"line":256,"column":46},"end":{"line":256,"column":58}},{"start":{"line":256,"column":58},"end":{"line":256,"column":67}}],"line":256},"56":{"loc":{"start":{"line":260,"column":17},"end":{"line":269,"column":null}},"type":"cond-expr","locations":[{"start":{"line":261,"column":18},"end":{"line":266,"column":null}},{"start":{"line":266,"column":18},"end":{"line":269,"column":null}}],"line":260},"57":{"loc":{"start":{"line":267,"column":21},"end":{"line":268,"column":35}},"type":"cond-expr","locations":[{"start":{"line":267,"column":35},"end":{"line":267,"column":77}},{"start":{"line":267,"column":76},"end":{"line":268,"column":35}}],"line":267},"58":{"loc":{"start":{"line":268,"column":21},"end":{"line":268,"column":null}},"type":"cond-expr","locations":[{"start":{"line":268,"column":35},"end":{"line":268,"column":60}},{"start":{"line":268,"column":60},"end":{"line":268,"column":null}}],"line":268},"59":{"loc":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"type":"cond-expr","locations":[{"start":{"line":274,"column":45},"end":{"line":274,"column":72}},{"start":{"line":274,"column":72},"end":{"line":274,"column":null}}],"line":274},"60":{"loc":{"start":{"line":275,"column":26},"end":{"line":275,"column":null}},"type":"binary-expr","locations":[{"start":{"line":275,"column":26},"end":{"line":275,"column":42}},{"start":{"line":275,"column":42},"end":{"line":275,"column":null}}],"line":275},"61":{"loc":{"start":{"line":277,"column":18},"end":{"line":279,"column":null}},"type":"cond-expr","locations":[{"start":{"line":278,"column":22},"end":{"line":278,"column":null}},{"start":{"line":279,"column":22},"end":{"line":279,"column":null}}],"line":277},"62":{"loc":{"start":{"line":277,"column":18},"end":{"line":277,"column":null}},"type":"binary-expr","locations":[{"start":{"line":277,"column":18},"end":{"line":277,"column":34}},{"start":{"line":277,"column":34},"end":{"line":277,"column":null}}],"line":277},"63":{"loc":{"start":{"line":283,"column":17},"end":{"line":283,"column":null}},"type":"cond-expr","locations":[{"start":{"line":283,"column":31},"end":{"line":283,"column":60}},{"start":{"line":283,"column":60},"end":{"line":283,"column":null}}],"line":283},"64":{"loc":{"start":{"line":287,"column":13},"end":{"line":317,"column":null}},"type":"binary-expr","locations":[{"start":{"line":287,"column":13},"end":{"line":287,"column":29}},{"start":{"line":287,"column":29},"end":{"line":287,"column":null}},{"start":{"line":288,"column":14},"end":{"line":317,"column":null}}],"line":287},"65":{"loc":{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},"type":"if","locations":[{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},{"start":{"line":302,"column":31},"end":{"line":305,"column":null}}],"line":297},"66":{"loc":{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},"type":"if","locations":[{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},{"start":{},"end":{}}],"line":300},"67":{"loc":{"start":{"line":311,"column":19},"end":{"line":316,"column":null}},"type":"binary-expr","locations":[{"start":{"line":311,"column":19},"end":{"line":311,"column":null}},{"start":{"line":312,"column":20},"end":{"line":316,"column":null}}],"line":311},"68":{"loc":{"start":{"line":322,"column":10},"end":{"line":376,"column":null}},"type":"binary-expr","locations":[{"start":{"line":322,"column":10},"end":{"line":322,"column":31}},{"start":{"line":322,"column":31},"end":{"line":322,"column":null}},{"start":{"line":323,"column":10},"end":{"line":376,"column":null}}],"line":322},"69":{"loc":{"start":{"line":327,"column":17},"end":{"line":342,"column":null}},"type":"binary-expr","locations":[{"start":{"line":327,"column":17},"end":{"line":327,"column":30}},{"start":{"line":327,"column":30},"end":{"line":327,"column":41}},{"start":{"line":327,"column":41},"end":{"line":327,"column":null}},{"start":{"line":328,"column":18},"end":{"line":342,"column":null}}],"line":327},"70":{"loc":{"start":{"line":344,"column":17},"end":{"line":359,"column":null}},"type":"binary-expr","locations":[{"start":{"line":344,"column":17},"end":{"line":344,"column":null}},{"start":{"line":345,"column":18},"end":{"line":359,"column":null}}],"line":344},"71":{"loc":{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},"type":"if","locations":[{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},{"start":{"line":351,"column":29},"end":{"line":353,"column":null}}],"line":349},"72":{"loc":{"start":{"line":379,"column":9},"end":{"line":470,"column":null}},"type":"binary-expr","locations":[{"start":{"line":379,"column":9},"end":{"line":379,"column":25}},{"start":{"line":379,"column":25},"end":{"line":470,"column":null}}],"line":379},"73":{"loc":{"start":{"line":380,"column":11},"end":{"line":397,"column":13}},"type":"cond-expr","locations":[{"start":{"line":381,"column":12},"end":{"line":387,"column":13}},{"start":{"line":387,"column":12},"end":{"line":397,"column":13}}],"line":380},"74":{"loc":{"start":{"line":381,"column":12},"end":{"line":387,"column":13}},"type":"binary-expr","locations":[{"start":{"line":381,"column":12},"end":{"line":381,"column":null}},{"start":{"line":382,"column":14},"end":{"line":387,"column":13}}],"line":381},"75":{"loc":{"start":{"line":397,"column":12},"end":{"line":470,"column":null}},"type":"binary-expr","locations":[{"start":{"line":397,"column":12},"end":{"line":397,"column":25}},{"start":{"line":397,"column":25},"end":{"line":397,"column":44}},{"start":{"line":397,"column":44},"end":{"line":397,"column":null}},{"start":{"line":398,"column":10},"end":{"line":470,"column":null}}],"line":397},"76":{"loc":{"start":{"line":416,"column":21},"end":{"line":419,"column":null}},"type":"binary-expr","locations":[{"start":{"line":416,"column":21},"end":{"line":416,"column":null}},{"start":{"line":417,"column":22},"end":{"line":419,"column":null}}],"line":416},"77":{"loc":{"start":{"line":423,"column":17},"end":{"line":431,"column":null}},"type":"binary-expr","locations":[{"start":{"line":423,"column":17},"end":{"line":423,"column":null}},{"start":{"line":424,"column":18},"end":{"line":431,"column":null}}],"line":423},"78":{"loc":{"start":{"line":449,"column":21},"end":{"line":452,"column":null}},"type":"binary-expr","locations":[{"start":{"line":449,"column":21},"end":{"line":449,"column":null}},{"start":{"line":450,"column":22},"end":{"line":452,"column":null}}],"line":449},"79":{"loc":{"start":{"line":456,"column":17},"end":{"line":464,"column":null}},"type":"binary-expr","locations":[{"start":{"line":456,"column":17},"end":{"line":456,"column":null}},{"start":{"line":457,"column":18},"end":{"line":464,"column":null}}],"line":456},"80":{"loc":{"start":{"line":505,"column":21},"end":{"line":505,"column":null}},"type":"binary-expr","locations":[{"start":{"line":505,"column":21},"end":{"line":505,"column":38}},{"start":{"line":505,"column":38},"end":{"line":505,"column":null}}],"line":505},"81":{"loc":{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},"type":"if","locations":[{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},{"start":{},"end":{}}],"line":508},"82":{"loc":{"start":{"line":512,"column":13},"end":{"line":513,"column":null}},"type":"binary-expr","locations":[{"start":{"line":512,"column":13},"end":{"line":512,"column":36}},{"start":{"line":512,"column":36},"end":{"line":513,"column":null}}],"line":512},"83":{"loc":{"start":{"line":514,"column":11},"end":{"line":537,"column":null}},"type":"binary-expr","locations":[{"start":{"line":514,"column":11},"end":{"line":514,"column":34}},{"start":{"line":514,"column":34},"end":{"line":514,"column":null}},{"start":{"line":515,"column":12},"end":{"line":537,"column":null}}],"line":514},"84":{"loc":{"start":{"line":533,"column":21},"end":{"line":534,"column":null}},"type":"binary-expr","locations":[{"start":{"line":533,"column":21},"end":{"line":533,"column":34}},{"start":{"line":533,"column":34},"end":{"line":534,"column":null}}],"line":533},"85":{"loc":{"start":{"line":539,"column":11},"end":{"line":539,"column":null}},"type":"binary-expr","locations":[{"start":{"line":539,"column":11},"end":{"line":539,"column":23}},{"start":{"line":539,"column":23},"end":{"line":539,"column":null}}],"line":539},"86":{"loc":{"start":{"line":540,"column":11},"end":{"line":540,"column":null}},"type":"binary-expr","locations":[{"start":{"line":540,"column":11},"end":{"line":540,"column":28}},{"start":{"line":540,"column":28},"end":{"line":540,"column":null}}],"line":540},"87":{"loc":{"start":{"line":544,"column":9},"end":{"line":566,"column":null}},"type":"binary-expr","locations":[{"start":{"line":544,"column":9},"end":{"line":544,"column":25}},{"start":{"line":544,"column":25},"end":{"line":566,"column":null}}],"line":544},"88":{"loc":{"start":{"line":554,"column":41},"end":{"line":554,"column":59}},"type":"cond-expr","locations":[{"start":{"line":554,"column":50},"end":{"line":554,"column":55}},{"start":{"line":554,"column":55},"end":{"line":554,"column":59}}],"line":554},"89":{"loc":{"start":{"line":556,"column":20},"end":{"line":556,"column":null}},"type":"cond-expr","locations":[{"start":{"line":556,"column":29},"end":{"line":556,"column":34}},{"start":{"line":556,"column":34},"end":{"line":556,"column":null}}],"line":556},"90":{"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":null}},"type":"cond-expr","locations":[{"start":{"line":558,"column":34},"end":{"line":558,"column":112}},{"start":{"line":558,"column":112},"end":{"line":558,"column":null}}],"line":558},"91":{"loc":{"start":{"line":578,"column":42},"end":{"line":578,"column":60}},"type":"cond-expr","locations":[{"start":{"line":578,"column":51},"end":{"line":578,"column":56}},{"start":{"line":578,"column":56},"end":{"line":578,"column":60}}],"line":578},"92":{"loc":{"start":{"line":580,"column":18},"end":{"line":582,"column":null}},"type":"cond-expr","locations":[{"start":{"line":581,"column":22},"end":{"line":581,"column":null}},{"start":{"line":582,"column":22},"end":{"line":582,"column":null}}],"line":580},"93":{"loc":{"start":{"line":593,"column":9},"end":{"line":623,"column":null}},"type":"binary-expr","locations":[{"start":{"line":593,"column":9},"end":{"line":593,"column":26}},{"start":{"line":593,"column":26},"end":{"line":593,"column":39}},{"start":{"line":593,"column":39},"end":{"line":593,"column":null}},{"start":{"line":594,"column":10},"end":{"line":623,"column":null}}],"line":593},"94":{"loc":{"start":{"line":600,"column":19},"end":{"line":605,"column":null}},"type":"binary-expr","locations":[{"start":{"line":600,"column":19},"end":{"line":600,"column":null}},{"start":{"line":601,"column":20},"end":{"line":605,"column":null}}],"line":600},"95":{"loc":{"start":{"line":607,"column":15},"end":{"line":622,"column":null}},"type":"binary-expr","locations":[{"start":{"line":607,"column":15},"end":{"line":607,"column":null}},{"start":{"line":608,"column":16},"end":{"line":622,"column":null}}],"line":607},"96":{"loc":{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},"type":"if","locations":[{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},{"start":{},"end":{}}],"line":612},"97":{"loc":{"start":{"line":612,"column":24},"end":{"line":612,"column":60}},"type":"binary-expr","locations":[{"start":{"line":612,"column":24},"end":{"line":612,"column":45}},{"start":{"line":612,"column":45},"end":{"line":612,"column":60}}],"line":612},"98":{"loc":{"start":{"line":628,"column":9},"end":{"line":636,"column":null}},"type":"binary-expr","locations":[{"start":{"line":628,"column":9},"end":{"line":628,"column":22}},{"start":{"line":628,"column":22},"end":{"line":628,"column":null}},{"start":{"line":629,"column":10},"end":{"line":636,"column":null}}],"line":628},"99":{"loc":{"start":{"line":638,"column":9},"end":{"line":645,"column":null}},"type":"binary-expr","locations":[{"start":{"line":638,"column":9},"end":{"line":638,"column":22}},{"start":{"line":638,"column":22},"end":{"line":638,"column":null}},{"start":{"line":639,"column":10},"end":{"line":645,"column":null}}],"line":638},"100":{"loc":{"start":{"line":654,"column":9},"end":{"line":662,"column":null}},"type":"binary-expr","locations":[{"start":{"line":654,"column":9},"end":{"line":654,"column":null}},{"start":{"line":655,"column":10},"end":{"line":662,"column":null}}],"line":654},"101":{"loc":{"start":{"line":661,"column":13},"end":{"line":661,"column":null}},"type":"cond-expr","locations":[{"start":{"line":661,"column":22},"end":{"line":661,"column":43}},{"start":{"line":661,"column":43},"end":{"line":661,"column":null}}],"line":661}},"s":{"0":1,"1":1,"2":199,"3":199,"4":199,"5":199,"6":199,"7":199,"8":199,"9":199,"10":199,"11":199,"12":199,"13":199,"14":199,"15":199,"16":199,"17":199,"18":199,"19":199,"20":199,"21":199,"22":199,"23":199,"24":199,"25":199,"26":199,"27":199,"28":199,"29":199,"30":199,"31":199,"32":199,"33":43,"34":32,"35":11,"36":1,"37":1,"38":10,"39":10,"40":10,"41":9,"42":9,"43":9,"44":1,"45":8,"46":8,"47":8,"48":8,"49":1,"50":7,"51":6,"52":7,"53":2,"54":2,"55":7,"56":3,"57":1,"58":2,"59":3,"60":10,"61":10,"62":199,"63":199,"64":5,"65":1,"66":4,"67":199,"68":15,"69":15,"70":15,"71":3,"72":4,"73":3,"74":15,"75":3,"76":3,"77":3,"78":3,"79":1,"80":2,"81":2,"82":1,"83":1,"84":3,"85":15,"86":1,"87":1,"88":1,"89":15,"90":15,"91":199,"92":5,"93":5,"94":1,"95":4,"96":4,"97":4,"98":199,"99":6,"100":6,"101":6,"102":2,"103":6,"104":1,"105":1,"106":5,"107":4,"108":4,"109":3,"110":3,"111":1,"112":199,"113":4,"114":4,"115":1,"116":3,"117":3,"118":2,"119":3,"120":199,"121":19,"122":7,"123":199,"124":199,"125":199,"126":5,"127":1,"128":2,"129":2,"130":6,"131":5,"132":8,"133":5,"134":3,"135":3,"136":2,"137":2,"138":1,"139":2,"140":2,"141":2,"142":2,"143":16,"144":2,"145":2,"146":2,"147":2,"148":2,"149":4,"150":2,"151":2,"152":2,"153":2,"154":2,"155":1,"156":1,"157":1,"158":1,"159":1,"160":10,"161":1,"162":1,"163":1,"164":17,"165":94,"166":11,"167":11,"168":11,"169":1,"170":2,"171":2,"172":60,"173":17,"174":17,"175":17,"176":1,"177":2,"178":1,"179":1,"180":1,"181":6,"182":2,"183":1,"184":4,"185":1,"186":1,"187":1,"188":1,"189":1,"190":1,"191":664,"192":664,"193":2,"194":1194,"195":1194,"196":2,"197":33,"198":1,"199":1,"200":2,"201":12,"202":15,"203":3,"204":3,"205":1},"f":{"0":199,"1":43,"2":9,"3":2,"4":3,"5":10,"6":5,"7":15,"8":3,"9":4,"10":3,"11":1,"12":5,"13":4,"14":6,"15":4,"16":3,"17":4,"18":2,"19":19,"20":7,"21":5,"22":1,"23":2,"24":2,"25":6,"26":5,"27":8,"28":5,"29":1,"30":2,"31":2,"32":16,"33":2,"34":2,"35":4,"36":2,"37":2,"38":2,"39":2,"40":1,"41":1,"42":10,"43":1,"44":1,"45":1,"46":17,"47":94,"48":11,"49":1,"50":2,"51":2,"52":60,"53":17,"54":1,"55":2,"56":1,"57":1,"58":1,"59":6,"60":2,"61":4,"62":1,"63":664,"64":2,"65":1194,"66":2,"67":33,"68":1,"69":1,"70":2,"71":12,"72":15,"73":1},"b":{"0":[199],"1":[199],"2":[199,190],"3":[199,197],"4":[199,1],"5":[199,174],"6":[199,196],"7":[199,196],"8":[199,196],"9":[199,192],"10":[199,192],"11":[199,173],"12":[199,173],"13":[199,193],"14":[199,2],"15":[32,11],"16":[43,12,12,11],"17":[1,10],"18":[1,8],"19":[1,7],"20":[6,1],"21":[6,3,3],"22":[2,5],"23":[7,7],"24":[2,2],"25":[1,2],"26":[2,1],"27":[199,149,139,136,136,129,125,124,102,102,99,89],"28":[1,4],"29":[5,4,3,2],"30":[15,13],"31":[15,13],"32":[15,12],"33":[15,13],"34":[15,14],"35":[15,13],"36":[1,14],"37":[15,2,1,1],"38":[3,12],"39":[15,3],"40":[1,1],"41":[1,14],"42":[15,1],"43":[1,4],"44":[2,4],"45":[1,5],"46":[3,1],"47":[1,3],"48":[2,1],"49":[113,86],"50":[113,86],"51":[199,166],"52":[3,163],"53":[3,1],"54":[199,196],"55":[1,1],"56":[1,195],"57":[33,162],"58":[33,162],"59":[1,5],"60":[196,163],"61":[13,183],"62":[196,163],"63":[33,163],"64":[196,163,13],"65":[3,2],"66":[1,1],"67":[13,5],"68":[199,188,21],"69":[16,14,4,4],"70":[16,14],"71":[1,1],"72":[199,166],"73":[3,163],"74":[3,1],"75":[166,3,2,164],"76":[94,93],"77":[164,163],"78":[60,59],"79":[164,163],"80":[199,188],"81":[1,1],"82":[199,25],"83":[199,11,3],"84":[4,3],"85":[199,10],"86":[199,5],"87":[199,166],"88":[1,1],"89":[7,657],"90":[7,657],"91":[1,1],"92":[11,1183],"93":[199,166,3,164],"94":[33,32],"95":[164,163],"96":[3,12],"97":[15,13],"98":[199,196,1],"99":[199,196,33],"100":[199,196],"101":[16,180]},"meta":{"lastBranch":102,"lastFunction":74,"lastStatement":206,"seen":{"s:16:18:16:Infinity":0,"s:17:18:17:Infinity":1,"f:43:24:43:43":0,"b:44:39:44:46":0,"b:44:57:44:64":1,"s:46:10:46:Infinity":2,"s:47:8:47:Infinity":3,"s:49:16:49:Infinity":4,"b:49:37:49:52:49:52:49:54":2,"s:50:16:50:Infinity":5,"b:50:37:50:52:50:52:50:54":3,"s:51:20:51:Infinity":6,"b:51:45:51:65:51:65:51:103":4,"s:52:20:52:Infinity":7,"b:52:45:52:78:52:78:52:80":5,"s:53:23:53:Infinity":8,"b:53:51:53:74:53:74:53:76":6,"s:54:22:54:Infinity":9,"b:54:64:54:86:54:86:54:90":7,"s:55:22:55:Infinity":10,"b:55:64:55:86:55:86:55:90":8,"s:56:24:56:Infinity":11,"s:57:26:57:Infinity":12,"s:58:30:58:Infinity":13,"s:59:8:59:Infinity":14,"s:60:15:60:Infinity":15,"b:60:35:60:49:60:49:60:51":9,"s:61:18:61:Infinity":16,"b:61:41:61:58:61:58:61:60":10,"s:62:15:62:Infinity":17,"b:62:45:62:70:62:70:62:72":11,"s:63:15:63:Infinity":18,"b:63:45:63:70:63:70:63:72":12,"s:64:15:64:Infinity":19,"b:64:45:64:59:64:59:64:61":13,"s:65:19:65:Infinity":20,"s:66:17:66:Infinity":21,"b:66:72:66:88:66:88:66:90":14,"s:67:23:67:Infinity":22,"s:68:25:68:Infinity":23,"s:69:26:69:Infinity":24,"s:70:17:70:Infinity":25,"s:71:22:71:Infinity":26,"s:72:19:72:Infinity":27,"s:73:24:73:Infinity":28,"s:74:25:74:Infinity":29,"s:75:8:75:Infinity":30,"s:76:8:76:Infinity":31,"s:78:2:113:Infinity":32,"f:78:2:78:18":1,"b:79:4:79:Infinity:undefined:undefined:undefined:undefined":15,"s:79:4:79:Infinity":33,"b:79:8:79:25:79:25:79:37:79:37:79:67:79:67:79:95":16,"s:79:95:79:Infinity":34,"b:80:4:83:Infinity:undefined:undefined:undefined:undefined":17,"s:80:4:83:Infinity":35,"s:81:6:81:Infinity":36,"s:82:6:82:Infinity":37,"s:85:17:85:Infinity":38,"s:86:4:86:Infinity":39,"s:87:4:110:Infinity":40,"f:87:45:87:51":2,"s:88:18:88:Infinity":41,"s:89:18:89:Infinity":42,"b:90:6:90:Infinity:undefined:undefined:undefined:undefined":18,"s:90:6:90:Infinity":43,"s:90:19:90:Infinity":44,"s:91:6:91:Infinity":45,"s:92:6:92:Infinity":46,"s:94:43:97:Infinity":47,"b:98:6:98:Infinity:undefined:undefined:undefined:undefined":19,"s:98:6:98:Infinity":48,"s:98:19:98:Infinity":49,"b:99:6:101:Infinity:undefined:undefined:undefined:undefined":20,"s:99:6:101:Infinity":50,"s:100:8:100:Infinity":51,"b:100:24:100:50:100:50:100:79:100:79:100:81":21,"b:102:6:104:Infinity:undefined:undefined:undefined:undefined":22,"s:102:6:104:Infinity":52,"b:102:10:102:50:102:50:102:78":23,"s:103:8:103:Infinity":53,"f:103:8:103:19":3,"s:103:30:103:122":54,"b:103:30:103:41:103:30:103:122":24,"s:105:6:105:Infinity":55,"f:106:4:106:7":4,"b:107:6:107:Infinity:undefined:undefined:undefined:undefined":25,"s:107:6:107:Infinity":56,"s:107:19:107:Infinity":57,"s:108:6:108:Infinity":58,"b:108:23:108:40:108:40:108:57":26,"s:109:6:109:Infinity":59,"s:112:4:112:Infinity":60,"f:112:4:112:17":5,"s:112:19:112:34":61,"s:116:4:127:Infinity":62,"b:116:4:116:Infinity:117:4:117:Infinity:118:4:118:Infinity:119:4:119:Infinity:120:4:120:Infinity:121:4:121:Infinity:122:4:122:Infinity:123:4:123:Infinity:124:4:124:Infinity:125:4:125:Infinity:126:4:126:Infinity:127:4:127:Infinity":27,"s:129:8:132:Infinity":63,"f:129:8:129:28":6,"b:130:4:130:Infinity:undefined:undefined:undefined:undefined":28,"s:130:4:130:Infinity":64,"b:130:8:130:24:130:24:130:37:130:37:130:48:130:48:130:108":29,"s:130:108:130:Infinity":65,"s:131:4:131:Infinity":66,"s:134:21:179:Infinity":67,"f:134:21:134:33":7,"s:135:4:135:Infinity":68,"s:136:4:178:Infinity":69,"s:137:22:152:Infinity":70,"b:138:15:138:24:138:24:138:Infinity":30,"b:139:15:139:24:139:24:139:Infinity":31,"b:141:20:141:33:141:33:141:Infinity":32,"b:142:23:142:39:142:39:142:Infinity":33,"b:145:14:145:22:145:22:145:Infinity":34,"b:146:17:146:28:146:28:146:Infinity":35,"f:147:19:147:26":8,"s:147:33:147:43":71,"f:148:32:148:39":9,"s:148:44:148:52":72,"f:148:66:148:73":10,"s:148:78:148:86":73,"b:150:12:150:Infinity:151:12:151:Infinity":36,"b:149:14:149:44:149:44:149:60:149:60:149:87:149:87:149:Infinity":37,"b:153:6:169:Infinity:undefined:undefined:undefined:undefined":38,"s:153:6:169:Infinity":74,"b:153:10:153:37:153:37:153:46":39,"s:154:25:154:Infinity":75,"s:155:8:155:Infinity":76,"s:156:8:168:Infinity":77,"s:157:29:159:Infinity":78,"f:158:12:158:24":11,"s:158:29:158:Infinity":79,"s:160:10:160:Infinity":80,"b:161:10:163:Infinity:undefined:undefined:undefined:undefined":40,"s:161:10:163:Infinity":81,"s:162:12:162:Infinity":82,"s:165:10:165:Infinity":83,"s:167:10:167:Infinity":84,"b:170:6:174:Infinity:undefined:undefined:undefined:undefined":41,"s:170:6:174:Infinity":85,"b:170:10:170:39:170:39:170:48":42,"s:171:8:173:Infinity":86,"s:172:10:172:Infinity":87,"s:172:16:172:61":88,"s:175:6:175:Infinity":89,"s:177:6:177:Infinity":90,"s:181:27:186:Infinity":91,"f:181:27:181:34":12,"s:182:18:182:Infinity":92,"b:183:4:183:Infinity:undefined:undefined:undefined:undefined":43,"s:183:4:183:Infinity":93,"s:183:24:183:Infinity":94,"s:184:23:184:Infinity":95,"s:185:4:185:Infinity":96,"f:185:4:185:20":13,"s:185:28:185:52":97,"s:188:8:206:Infinity":98,"f:188:8:188:26":14,"s:189:4:189:Infinity":99,"s:190:4:190:Infinity":100,"b:191:4:191:Infinity:undefined:undefined:undefined:undefined":44,"s:191:4:191:Infinity":101,"s:191:34:191:Infinity":102,"b:192:4:195:Infinity:undefined:undefined:undefined:undefined":45,"s:192:4:195:Infinity":103,"s:193:6:193:Infinity":104,"s:194:6:194:Infinity":105,"s:196:4:205:Infinity":106,"f:196:42:196:54":15,"s:197:6:204:Infinity":107,"s:198:20:198:Infinity":108,"s:199:8:201:Infinity":109,"b:199:28:199:42:199:42:199:46":46,"f:199:58:199:63":16,"s:199:150:201:10":110,"s:203:8:203:Infinity":111,"s:208:8:213:Infinity":112,"f:208:8:208:23":17,"s:209:18:209:Infinity":113,"b:210:4:210:Infinity:undefined:undefined:undefined:undefined":47,"s:210:4:210:Infinity":114,"s:210:16:210:Infinity":115,"b:211:4:211:Infinity:undefined:undefined:undefined:undefined":48,"s:211:4:211:Infinity":116,"s:211:31:211:Infinity":117,"f:211:31:211:39":18,"s:211:47:211:63":118,"s:212:4:212:Infinity":119,"s:215:33:215:Infinity":120,"f:215:47:215:54":19,"s:215:60:215:93":121,"f:215:68:215:73":20,"s:215:78:215:92":122,"s:217:18:217:Infinity":123,"s:218:21:218:Infinity":124,"s:220:2:665:Infinity":125,"b:226:34:226:65:226:65:226:Infinity":49,"b:230:28:230:59:230:59:230:Infinity":50,"b:238:9:238:26:238:26:246:Infinity":51,"b:239:10:241:11:241:10:246:Infinity":52,"b:239:73:239:82:239:82:239:124":53,"f:243:12:243:22":21,"s:243:27:243:Infinity":126,"b:249:9:249:Infinity:250:10:319:Infinity":54,"f:251:133:251:142":22,"s:251:149:251:191":127,"f:252:118:252:127":23,"s:252:134:252:176":128,"f:256:16:256:32":24,"s:256:16:256:Infinity":129,"b:256:46:256:58:256:58:256:67":55,"b:261:18:266:Infinity:266:18:269:Infinity":56,"b:267:35:267:77:267:76:268:35":57,"b:268:35:268:60:268:60:268:Infinity":58,"f:274:16:274:31":25,"s:274:31:274:Infinity":130,"b:274:45:274:72:274:72:274:Infinity":59,"f:274:72:274:91":26,"s:274:96:274:98":131,"b:275:26:275:42:275:42:275:Infinity":60,"b:278:22:278:Infinity:279:22:279:Infinity":61,"b:277:18:277:34:277:34:277:Infinity":62,"b:283:31:283:60:283:60:283:Infinity":63,"b:287:13:287:29:287:29:287:Infinity:288:14:317:Infinity":64,"f:290:42:290:46":27,"s:291:20:309:Infinity":132,"f:296:31:296:43":28,"b:297:24:305:Infinity:302:31:305:Infinity":65,"s:297:24:305:Infinity":133,"s:298:26:301:Infinity":134,"s:299:43:299:Infinity":135,"b:300:28:300:Infinity:undefined:undefined:undefined:undefined":66,"s:300:28:300:Infinity":136,"s:300:40:300:Infinity":137,"f:300:40:300:50":29,"s:300:58:300:75":138,"s:303:26:303:Infinity":139,"f:303:26:303:44":30,"s:303:52:303:68":140,"s:304:26:304:Infinity":141,"f:304:26:304:36":31,"s:304:44:304:57":142,"b:311:19:311:Infinity:312:20:316:Infinity":67,"b:322:10:322:31:322:31:322:Infinity:323:10:376:Infinity":68,"f:324:20:324:25":32,"s:325:14:361:Infinity":143,"b:327:17:327:30:327:30:327:41:327:41:327:Infinity:328:18:342:Infinity":69,"f:330:20:330:35":33,"s:331:22:337:Infinity":144,"f:331:22:331:32":34,"s:332:37:332:Infinity":145,"s:333:40:333:Infinity":146,"s:334:24:334:Infinity":147,"s:335:24:335:Infinity":148,"f:335:29:335:38":35,"s:335:50:335:115":149,"f:335:99:335:111":36,"s:336:24:336:Infinity":150,"b:344:17:344:Infinity:345:18:359:Infinity":70,"f:347:29:347:41":37,"s:348:22:348:Infinity":151,"f:348:22:348:32":38,"s:348:40:348:71":152,"f:348:45:348:52":39,"s:348:57:348:70":153,"b:349:22:353:Infinity:351:29:353:Infinity":71,"s:349:22:353:Infinity":154,"s:350:24:350:Infinity":155,"s:350:30:350:75":156,"s:352:24:352:Infinity":157,"f:352:24:352:42":40,"s:352:50:352:80":158,"f:352:55:352:62":41,"s:352:68:352:79":159,"f:363:26:363:31":42,"s:364:14:374:Infinity":160,"f:368:18:368:33":43,"s:368:33:368:Infinity":161,"f:368:33:368:49":44,"s:368:57:368:87":162,"f:368:62:368:70":45,"s:368:79:368:86":163,"b:379:9:379:25:379:25:470:Infinity":72,"b:381:12:387:13:387:12:397:13":73,"b:381:12:381:Infinity:382:14:387:13":74,"f:390:14:390:24":46,"s:390:29:390:Infinity":164,"b:397:12:397:25:397:25:397:44:397:44:397:Infinity:398:10:470:Infinity":75,"f:406:22:406:27":47,"s:407:18:421:Infinity":165,"f:412:22:412:32":48,"s:412:52:412:63":166,"s:412:63:412:89":167,"s:412:89:412:103":168,"b:416:21:416:Infinity:417:22:419:Infinity":76,"f:417:44:417:59":49,"s:417:59:417:100":169,"f:417:72:417:80":50,"s:417:89:417:96":170,"b:423:17:423:Infinity:424:18:431:Infinity":77,"f:426:20:426:35":51,"s:426:35:426:Infinity":171,"f:439:22:439:27":52,"s:440:18:454:Infinity":172,"f:445:22:445:32":53,"s:445:52:445:63":173,"s:445:63:445:89":174,"s:445:89:445:103":175,"b:449:21:449:Infinity:450:22:452:Infinity":78,"f:450:44:450:59":54,"s:450:59:450:100":176,"f:450:72:450:80":55,"s:450:89:450:96":177,"b:456:17:456:Infinity:457:18:464:Infinity":79,"f:459:20:459:35":56,"s:459:35:459:Infinity":178,"f:481:16:481:26":57,"s:481:31:481:Infinity":179,"f:493:16:493:26":58,"s:493:31:493:Infinity":180,"b:505:21:505:38:505:38:505:Infinity":80,"f:507:14:507:24":59,"s:507:29:507:Infinity":181,"f:508:14:508:29":60,"b:508:31:508:92:undefined:undefined:undefined:undefined":81,"s:508:31:508:92":182,"s:508:63:508:92":183,"b:512:13:512:36:512:36:513:Infinity":82,"b:514:11:514:34:514:34:514:Infinity:515:12:537:Infinity":83,"f:516:31:516:36":61,"s:517:16:535:Infinity":184,"f:520:18:520:33":62,"s:521:20:521:Infinity":185,"s:522:20:522:Infinity":186,"s:523:20:523:Infinity":187,"s:524:20:524:Infinity":188,"s:525:20:525:Infinity":189,"s:526:20:526:Infinity":190,"b:533:21:533:34:533:34:534:Infinity":84,"b:539:11:539:23:539:23:539:Infinity":85,"b:540:11:540:28:540:28:540:Infinity":86,"b:544:9:544:25:544:25:566:Infinity":87,"f:547:26:547:30":63,"s:548:29:548:Infinity":191,"s:549:14:562:Infinity":192,"f:554:18:554:33":64,"s:554:33:554:Infinity":193,"b:554:50:554:55:554:55:554:59":88,"b:556:29:556:34:556:34:556:Infinity":89,"b:558:34:558:112:558:112:558:Infinity":90,"f:571:27:571:31":65,"s:572:27:572:Infinity":194,"s:573:12:587:Infinity":195,"f:578:16:578:31":66,"s:578:31:578:Infinity":196,"b:578:51:578:56:578:56:578:60":91,"b:581:22:581:Infinity:582:22:582:Infinity":92,"b:593:9:593:26:593:26:593:39:593:39:593:Infinity:594:10:623:Infinity":93,"f:597:20:597:24":67,"s:598:16:605:Infinity":197,"b:600:19:600:Infinity:601:20:605:Infinity":94,"f:601:42:601:57":68,"s:601:57:601:103":198,"f:601:57:601:65":69,"s:601:73:601:100":199,"f:601:78:601:85":70,"s:601:90:601:99":200,"b:607:15:607:Infinity:608:16:622:Infinity":95,"f:610:18:610:28":71,"s:610:33:610:Infinity":201,"f:611:18:611:29":72,"b:612:20:615:Infinity:undefined:undefined:undefined:undefined":96,"s:612:20:615:Infinity":202,"b:612:24:612:45:612:45:612:60":97,"s:613:22:613:Infinity":203,"s:614:22:614:Infinity":204,"b:628:9:628:22:628:22:628:Infinity:629:10:636:Infinity":98,"b:638:9:638:22:638:22:638:Infinity:639:10:645:Infinity":99,"f:641:12:641:27":73,"s:641:27:641:Infinity":205,"b:654:9:654:Infinity:655:10:662:Infinity":100,"b:661:22:661:43:661:43:661:Infinity":101},"fnNames":{}}} -} diff --git a/client/src/api/client.behavior.test.ts b/client/src/api/client.behavior.test.ts index a5ff0cf08..71837160a 100644 --- a/client/src/api/client.behavior.test.ts +++ b/client/src/api/client.behavior.test.ts @@ -1,4 +1,4 @@ -// FE-APIWIRE-001 to FE-APIWIRE-035 +// FE-APIWIRE-001 to FE-APIWIRE-036 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { AxiosError, type AxiosAdapter, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' import { http, HttpResponse } from 'msw' @@ -184,6 +184,20 @@ describe('client > rate-limit translation', () => { const err = await captureError(() => apiClient.get('/limited')) expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' }) }) + + it('FE-APIWIRE-035: an array 429 body is replaced, not grafted onto', async () => { + server.use(http.get('/api/limited', () => HttpResponse.json([{ field: 'email' }], { status: 429 }))) + const err = await captureError(() => apiClient.get('/limited')) + expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' }) + }) + + it('FE-APIWIRE-036: Catalan, Greek and Vietnamese have their own 429 message', async () => { + for (const lang of ['ca', 'gr', 'vi']) { + localStorage.setItem('app_language', lang) + const err = await captureError(() => apiClient.get('/limited')) + expect(err.message).not.toBe('Too many attempts. Please try again later.') + } + }) }) describe('client > proxy auth challenges', () => { @@ -398,6 +412,7 @@ describe('client > adminApi.llmLocalPull', () => { read: async () => (i < chunks.length ? { done: false, value: encoder.encode(chunks[i++]) } : { done: true, value: undefined }), + cancel: async () => {}, }), }, } as unknown as Response @@ -450,13 +465,26 @@ describe('client > adminApi.llmLocalPull', () => { .rejects.toThrow('Pull failed (500)') }) - it('FE-APIWIRE-033: a 200 without a readable body is treated as a failure', async () => { + it('FE-APIWIRE-036: a throw from onProgress aborts the pull', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(streamingResponse([ + '{"status":"pulling manifest"}\n{"error":"manifest not found"}\n{"status":"success"}\n', + ])) + const onProgress = vi.fn((p: { error?: string }) => { + if (p.error) throw new Error(p.error) + }) + + await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', onProgress)) + .rejects.toThrow('manifest not found') + expect(onProgress).toHaveBeenCalledTimes(2) + }) + + it('FE-APIWIRE-033: a 200 without a readable body reports the missing stream', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, status: 200, body: null, json: async () => ({}), } as unknown as Response) await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', vi.fn())) - .rejects.toThrow('Pull failed (200)') + .rejects.toThrow('Pull returned no progress stream') }) }) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index f3d9c833f..8d355bf48 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -105,6 +105,9 @@ const RATE_LIMIT_MESSAGES: Record = { ko: '시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.', uk: 'Занадто багато спроб. Спробуйте пізніше.', sv: 'För många försök. Prova igen senare.', + ca: 'Massa intents. Torneu-ho a provar més tard.', + gr: 'Πάρα πολλές προσπάθειες. Δοκιμάστε ξανά αργότερα.', + vi: 'Quá nhiều lần thử. Vui lòng thử lại sau.', } function translateRateLimit(): string { @@ -228,9 +231,11 @@ apiClient.interceptors.response.use( } if (error.response?.status === 429) { const translated = translateRateLimit() - const data = error.response.data as { error?: string } | undefined - if (data && typeof data === 'object') { - data.error = translated + const data = error.response.data + // Only a plain object body carries an `error` field worth overwriting; + // an array (a validation-error list) or a string is replaced outright. + if (data && typeof data === 'object' && !Array.isArray(data)) { + (data as { error?: string }).error = translated } else { error.response.data = { error: translated } } @@ -551,24 +556,35 @@ export const adminApi = { headers: { 'content-type': 'application/json' }, body: JSON.stringify({ baseUrl, model }), }) - if (!res.ok || !res.body) { + if (!res.ok) { let msg = `Pull failed (${res.status})` try { msg = (await res.json())?.error ?? msg } catch { /* non-json */ } throw new Error(msg) } + // An accepted request without a stream can't be followed to completion. + if (!res.body) throw new Error('Pull returned no progress stream') const reader = res.body.getReader() const dec = new TextDecoder() let buf = '' - for (;;) { - const { done, value } = await reader.read() - if (done) break - buf += dec.decode(value, { stream: true }) - const lines = buf.split('\n') - buf = lines.pop() ?? '' - for (const line of lines) { - if (!line.trim()) continue - try { onProgress(JSON.parse(line)) } catch { /* skip partial */ } + try { + for (;;) { + const { done, value } = await reader.read() + if (done) break + buf += dec.decode(value, { stream: true }) + // split() always yields at least one element; the last one is the + // trailing (possibly partial) line carried into the next chunk. + const lines = buf.split('\n') + buf = lines.pop()! + for (const line of lines) { + if (!line.trim()) continue + // Only the parse is swallowed — a throw from onProgress aborts the pull. + let frame: { status?: string; total?: number; completed?: number; error?: string } + try { frame = JSON.parse(line) } catch { continue } + onProgress(frame) + } } + } finally { + reader.cancel().catch(() => {}) } }, checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data), diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 20c6c1a5c..6f13fcf7d 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -560,7 +560,7 @@ describe('AddonManager', () => { }); }); - it('FE-ADMIN-ADDON-027: an error frame in the pull stream is tolerated and the row returns to idle', async () => { + it('FE-ADMIN-ADDON-027: an error frame in the pull stream aborts the pull and is reported', async () => { const user = userEvent.setup(); server.use( addonsRoute([llmAddon({ provider: 'local' })]), @@ -575,6 +575,8 @@ describe('AddonManager', () => { await screen.findByText('No models installed yet — pull one below.'); await user.click(screen.getByRole('button', { name: 'Pull' })); + await screen.findByText('manifest not found'); + expect(screen.queryByText('Model pulled')).not.toBeInTheDocument(); await waitFor(() => expect(screen.getByRole('button', { name: 'Pull' })).toBeEnabled()); expect(screen.queryByText('Pulling…')).not.toBeInTheDocument(); }); diff --git a/client/src/components/Admin/AdminPluginsPanel.test.tsx b/client/src/components/Admin/AdminPluginsPanel.test.tsx index 74da6b02f..7fff27480 100644 --- a/client/src/components/Admin/AdminPluginsPanel.test.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.test.tsx @@ -1242,6 +1242,21 @@ describe('AdminPluginsPanel — Discover cards and the detail modal', () => { expect(screen.queryByText('Setup')).not.toBeInTheDocument() }) + it('FE-COMP-PLUGINS-PANEL-043b: a manifest that omits the lists still renders the modal', async () => { + const detail = manifestDetail({ size: 0 }) + delete (detail.manifest as Record).egress + delete (detail.manifest as Record).settings + delete (detail.manifest as Record).permissions + discoverWith({}, detail) + render() + await clickDiscover() + fireEvent.click(await screen.findByText('Gotify')) + + expect(await screen.findByText('Needs no special access.')).toBeInTheDocument() + expect(screen.queryByText('Connects to')).not.toBeInTheDocument() + expect(screen.queryByText('Setup')).not.toBeInTheDocument() + }) + it('FE-COMP-PLUGINS-PANEL-044: an incompatible entry explains the blocked button in the modal body', async () => { discoverWith({ compatible: false, latestCompatible: null, trek: '>=4.0.0', hostVersion: '3.3.0' }) render() diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx index 2d0baa95b..de5d42c54 100644 --- a/client/src/components/Admin/AdminPluginsPanel.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.tsx @@ -96,15 +96,19 @@ interface RegistryDetail extends RegistryItem { size: number | null publishedAt: string | null manifest: { - permissions: string[] - egress: string[] + // Optional because a registry may omit an empty list — the detail view has to + // degrade rather than throw halfway through its render. + permissions?: string[] + egress?: string[] /** The plugin needs OPERATOR-supplied hosts — its egress list is not the whole story. */ operatorEgress?: boolean - settings: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }> + settings?: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }> license: string | null icon: string | null requiredAddons?: string[] pluginDependencies?: PluginDep[] + /** Display slice of the manifest's capabilities — drives the same chips as an installed row. */ + capabilities?: { widget?: { slot?: string }; tripPage?: { replaces?: string[] } } } | null } @@ -300,13 +304,8 @@ function installOffer(item: RegistryItem, t: T): { blocked: boolean; version?: s return { blocked: true, label: t('admin.plugins.incompatible'), title } } -function ReviewedBadge({ t, compact }: { t: T; compact?: boolean }) { - if (compact) return - return ( - - {t('admin.plugins.reviewed')} - - ) +function ReviewedBadge({ t }: { t: T }) { + return } /** Marks a manually-uploaded (sideloaded) plugin: no registry, unsigned, not reviewed. */ @@ -1149,7 +1148,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked
{p.name} {p.version && v{p.version}} - {p.reviewed_at && } + {p.reviewed_at && } {p.source_repo === 'local:upload' && } {p.source_repo === 'local:link' && } {/* Registry plugins only — a sideloaded/dev-linked plugin already says something @@ -1401,7 +1400,10 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local }, [item.id]) const manifest = detail?.manifest ?? null - const caps = manifest ? deriveCaps(manifest.permissions, {}, t) : [] + const permissions = manifest?.permissions ?? [] + const egress = manifest?.egress ?? [] + const settings = manifest?.settings ?? [] + const caps = manifest ? deriveCaps(permissions, manifest.capabilities ?? {}, t) : [] const repoUrl = `https://github.com/${item.repo}` const homepage = item.homepage && /^https?:\/\//i.test(item.homepage) && item.homepage !== repoUrl ? item.homepage : null const sizeKb = detail?.size ? Math.max(1, Math.round(detail.size / 1024)) : null @@ -1425,7 +1427,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local

{item.name}

- {item.reviewedAt && } + {item.reviewedAt && }

{item.author}{item.latest ? ` · v${item.latest}` : ''}

@@ -1451,7 +1453,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local {manifest && (

{t('admin.plugins.accessTitle')}

- {caps.filter(c => !c.net).length === 0 && !manifest.permissions.includes('db:own') ? ( + {caps.filter(c => !c.net).length === 0 && !permissions.includes('db:own') ? (

{t('admin.plugins.noAccess')}

) : (
@@ -1460,7 +1462,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local {c.label}
))} - {manifest.permissions.includes('db:own') && ( + {permissions.includes('db:own') && (
{t('admin.plugins.perm.db:own')}
@@ -1470,11 +1472,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
)} - {manifest && (manifest.egress.length > 0 || manifest.operatorEgress) && ( + {manifest && (egress.length > 0 || manifest.operatorEgress) && (

{t('admin.plugins.connectsTitle')}

- {manifest.egress.map(h => ( + {egress.map(h => ( {h} ))} {/* The hosts above are NOT the whole story for this plugin: it talks to a @@ -1493,11 +1495,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
)} - {manifest && manifest.settings.length > 0 && ( + {manifest && settings.length > 0 && (

{t('admin.plugins.setupTitle')}

    - {manifest.settings.map(s => ( + {settings.map(s => (
  • {s.label} {t(`admin.plugins.scope.${s.scope}` as never)} diff --git a/client/src/components/Admin/CategoryManager.tsx b/client/src/components/Admin/CategoryManager.tsx index 687378966..6c68f4bc0 100644 --- a/client/src/components/Admin/CategoryManager.tsx +++ b/client/src/components/Admin/CategoryManager.tsx @@ -56,8 +56,8 @@ export default function CategoryManager() { setEditingId(null) } + // The Save button carries disabled={… || !form.name.trim()}, so the name is set here. const handleSave = async () => { - if (!form.name.trim()) { toast.error(t('categories.toast.nameRequired')); return } setIsSaving(true) try { if (editingId) { diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index c9121edec..d7bd03b2b 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -48,8 +48,8 @@ function normalizeProvider(value: unknown): MapProvider { return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet' } -function styleForProvider(provider: MapProvider, style?: string | null): string { - if (provider === 'leaflet') return style || MAPBOX_DEFAULT_STYLE +/** Only the GL providers keep a style — Leaflet is handled by its callers. */ +function styleForProvider(provider: GlMapProvider, style?: string | null): string { if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE return normalizeStyleForProvider(provider, style) } diff --git a/client/src/components/Admin/DevNotificationsPanel.test.tsx b/client/src/components/Admin/DevNotificationsPanel.test.tsx index 9402586ab..35932e52e 100644 --- a/client/src/components/Admin/DevNotificationsPanel.test.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.test.tsx @@ -110,7 +110,20 @@ describe('DevNotificationsPanel', () => { }); }); - it('FE-ADMIN-DEVNOTIF-008: error toast shown on API failure', async () => { + it('FE-ADMIN-DEVNOTIF-008: the server error field is what the toast shows', async () => { + server.use( + http.post('/api/admin/dev/test-notification', () => + HttpResponse.json({ error: 'No channel configured' }, { status: 500 }), + ), + ); + const user = userEvent.setup(); + render(<>); + await screen.findByText('Type Testing'); + await user.click(screen.getByText('Simple → Me').closest('button')!); + await screen.findByText('No channel configured'); + }); + + it('FE-ADMIN-DEVNOTIF-008b: a failure without an error field falls back to the generic text', async () => { server.use( http.post('/api/admin/dev/test-notification', () => HttpResponse.json({ message: 'Server error' }, { status: 500 }), @@ -120,7 +133,7 @@ describe('DevNotificationsPanel', () => { render(<>); await screen.findByText('Type Testing'); await user.click(screen.getByText('Simple → Me').closest('button')!); - await screen.findByText(/failed|error/i); + await screen.findByText('Failed'); }); it('FE-ADMIN-DEVNOTIF-009: changing trip selector updates payload targetId', async () => { diff --git a/client/src/components/Admin/DevNotificationsPanel.tsx b/client/src/components/Admin/DevNotificationsPanel.tsx index f5322e739..7237b23ae 100644 --- a/client/src/components/Admin/DevNotificationsPanel.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react' import { adminApi, tripsApi } from '../../api/client' +import { getApiErrorMessage } from '../../utils/apiError' import { useAuthStore } from '../../store/authStore' import { useToast } from '../shared/Toast' import { @@ -46,8 +47,8 @@ export default function DevNotificationsPanel(): React.ReactElement { try { await adminApi.sendTestNotification(payload) toast.success(`Sent: ${label}`) - } catch (err: any) { - toast.error(err.message || 'Failed') + } catch (err: unknown) { + toast.error(getApiErrorMessage(err, 'Failed')) } finally { setSending(null) } diff --git a/client/src/components/Admin/PackingTemplateManager.test.tsx b/client/src/components/Admin/PackingTemplateManager.test.tsx index 1e150a2aa..8abb51cde 100644 --- a/client/src/components/Admin/PackingTemplateManager.test.tsx +++ b/client/src/components/Admin/PackingTemplateManager.test.tsx @@ -697,7 +697,7 @@ describe('PackingTemplateManager', () => { await user.click(categoryButtons('Clothing')[2]); - expect(await screen.findByText('Failed to delete template')).toBeInTheDocument(); + expect(await screen.findByText('Failed to delete category')).toBeInTheDocument(); expect(screen.getByText('Clothing')).toBeInTheDocument(); expect(screen.getByText('T-shirt')).toBeInTheDocument(); }); @@ -798,7 +798,7 @@ describe('PackingTemplateManager', () => { await user.click(itemButtons('T-shirt')[1]); - expect(await screen.findByText('Failed to delete template')).toBeInTheDocument(); + expect(await screen.findByText('Failed to delete item')).toBeInTheDocument(); expect(screen.getByText('T-shirt')).toBeInTheDocument(); }); diff --git a/client/src/components/Admin/PackingTemplateManager.tsx b/client/src/components/Admin/PackingTemplateManager.tsx index adcbe9a0c..9773b1999 100644 --- a/client/src/components/Admin/PackingTemplateManager.tsx +++ b/client/src/components/Admin/PackingTemplateManager.tsx @@ -115,12 +115,13 @@ export default function PackingTemplateManager() { await adminApi.deleteTemplateCategory(expandedId, catId) setCategories(prev => prev.filter(c => c.id !== catId)) setItems(prev => prev.filter(i => i.category_id !== catId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.packingTemplates.deleteCategoryError')) } } // Item CRUD const handleAddItem = async (catId: number) => { - if (!newItemName.trim() || !expandedId) return + // The name is already guaranteed non-empty by the button and the Enter handler. + if (!expandedId) return try { const data = await adminApi.addTemplateItem(expandedId, catId, { name: newItemName.trim() }) setItems(prev => [...prev, data.item]) @@ -143,7 +144,7 @@ export default function PackingTemplateManager() { try { await adminApi.deleteTemplateItem(expandedId, itemId) setItems(prev => prev.filter(i => i.id !== itemId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.packingTemplates.deleteItemError')) } } const inputStyle = 'w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent outline-none' diff --git a/client/src/components/Budget/BudgetPanel.helpers.test.ts b/client/src/components/Budget/BudgetPanel.helpers.test.ts index 438affa14..4a72cf3bb 100644 --- a/client/src/components/Budget/BudgetPanel.helpers.test.ts +++ b/client/src/components/Budget/BudgetPanel.helpers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { calcPP, hasCustomMemberSplit } from './BudgetPanel.helpers' +import { calcPP, hasCustomMemberSplit, normalizePastedAmount } from './BudgetPanel.helpers' describe('BudgetPanel.helpers', () => { describe('hasCustomMemberSplit (#1458)', () => { @@ -25,4 +25,18 @@ describe('BudgetPanel.helpers', () => { expect(calcPP(100, 0)).toBeNull() expect(calcPP(100, null)).toBeNull() }) + + describe('normalizePastedAmount', () => { + it('keeps the last separator as the decimal point', () => { + expect(normalizePastedAmount('1.234,56 €')).toBe('1234.56') + expect(normalizePastedAmount('$1,234.56')).toBe('1234.56') + expect(normalizePastedAmount(' -12,5 ')).toBe('-12.5') + }) + + it('drops everything that is not part of the number', () => { + expect(normalizePastedAmount('EUR 1 234 567')).toBe('1234567') + expect(normalizePastedAmount('42')).toBe('42') + expect(normalizePastedAmount('abc')).toBe('') + }) + }) }) diff --git a/client/src/components/Budget/BudgetPanel.helpers.ts b/client/src/components/Budget/BudgetPanel.helpers.ts index c3b13fdc9..987b4a18d 100644 --- a/client/src/components/Budget/BudgetPanel.helpers.ts +++ b/client/src/components/Budget/BudgetPanel.helpers.ts @@ -77,3 +77,15 @@ export function splitColorFor(userId: number, order: number) { export function colorForUserId(userId: number) { return SPLIT_COLORS[((userId | 0) - 1 + SPLIT_COLORS.length * 1000) % SPLIT_COLORS.length] } + +/** + * Normalises a pasted amount to a plain `1234.56` string: drops currency + * symbols and spaces, treats the last comma/dot as the decimal separator and + * removes every thousand separator before it. + */ +export function normalizePastedAmount(raw: string): string { + const text = raw.trim().replace(/[^\d.,-]/g, '') + const decimalPos = Math.max(text.lastIndexOf(','), text.lastIndexOf('.')) + if (decimalPos === -1) return text.replace(/[.,]/g, '') + return text.substring(0, decimalPos).replace(/[.,]/g, '') + '.' + text.substring(decimalPos + 1) +} diff --git a/client/src/components/Budget/BudgetPanelAddItemRow.tsx b/client/src/components/Budget/BudgetPanelAddItemRow.tsx index 484cddaa2..5f9deb4fa 100644 --- a/client/src/components/Budget/BudgetPanelAddItemRow.tsx +++ b/client/src/components/Budget/BudgetPanelAddItemRow.tsx @@ -1,6 +1,7 @@ import { useState, useRef } from 'react' import { Plus } from 'lucide-react' import { CustomDatePicker } from '../shared/CustomDateTimePicker' +import { normalizePastedAmount } from './BudgetPanel.helpers' interface AddItemRowProps { onAdd: (data: { name: string; total_price: number; persons: number | null; days: number | null; note: string | null; expense_date: string | null }) => void @@ -33,7 +34,7 @@ export default function AddItemRow({ onAdd, t }: AddItemRowProps) { setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } setPrice(t) }} + onPaste={e => { e.preventDefault(); setPrice(normalizePastedAmount(e.clipboardData.getData('text'))) }} placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> diff --git a/client/src/components/Budget/BudgetPanelInlineEditCell.tsx b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx index 5dae2a574..159bd500d 100644 --- a/client/src/components/Budget/BudgetPanelInlineEditCell.tsx +++ b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react' +import { normalizePastedAmount } from './BudgetPanel.helpers' interface InlineEditCellProps { value: string | number | null | undefined @@ -29,21 +30,7 @@ export default function InlineEditCell({ value, onSave, type = 'text', style = { const handlePaste = (e: React.ClipboardEvent) => { if (type !== 'number') return e.preventDefault() - let text = e.clipboardData.getData('text').trim() - // Strip everything except digits, dots, commas, minus - text = text.replace(/[^\d.,-]/g, '') - // Remove all thousand separators (dots or commas before 3-digit groups), keep last separator as decimal - const lastComma = text.lastIndexOf(',') - const lastDot = text.lastIndexOf('.') - const decimalPos = Math.max(lastComma, lastDot) - if (decimalPos > -1) { - const intPart = text.substring(0, decimalPos).replace(/[.,]/g, '') - const decPart = text.substring(decimalPos + 1) - text = intPart + '.' + decPart - } else { - text = text.replace(/[.,]/g, '') - } - setEditValue(text) + setEditValue(normalizePastedAmount(e.clipboardData.getData('text'))) } if (editing) { diff --git a/client/src/components/Budget/CostsPanel.test.tsx b/client/src/components/Budget/CostsPanel.test.tsx index 19e1b2686..d84f687e7 100644 --- a/client/src/components/Budget/CostsPanel.test.tsx +++ b/client/src/components/Budget/CostsPanel.test.tsx @@ -170,6 +170,35 @@ describe('CostsPanel — settlements in the ledger', () => { expect(screen.getByText('Unfinished')).toBeInTheDocument() }) + it('shows the net hint on a settled expense row and hides it while unfinished', async () => { + seedStore(useAuthStore, { user: buildUser({ id: 1, username: 'alice' }), isAuthenticated: true }) + seedStore(useSettingsStore, { settings: { ...useSettingsStore.getState().settings, default_currency: 'EUR' } }) + // alice fronted the whole 90 for a two-way split, so she is 45 up. + const lent = { + ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), + total_price: 90, + payers: [{ user_id: 1, amount: 90, username: 'alice' }], + members: [{ user_id: 1, username: 'alice', paid: 1 }, { user_id: 2, username: 'bob', paid: 0 }], + } + const unpaid = { + ...buildBudgetItem({ trip_id: 1, category: 'lodging', name: 'Hotel' }), + total_price: 90, + payers: [], + members: [{ user_id: 1, username: 'alice', paid: 0 }, { user_id: 2, username: 'bob', paid: 0 }], + } + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [lent, unpaid] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + ) + render() + + const dinnerRow = (await screen.findByText('Dinner')).closest('.exp-row') as HTMLElement + expect(within(dinnerRow).getByText(/you lent/)).toHaveTextContent('45') + + const hotelRow = (await screen.findByText('Hotel')).closest('.exp-row') as HTMLElement + expect(within(hotelRow).queryByText(/you lent|you borrowed/)).toBeNull() + }) + it('sums only unfinished expenses in the Outstanding amount card', async () => { // Display in the trip's own currency so FX conversion is an identity — keeps the asserted sum deterministic. seedStore(useSettingsStore, { settings: { ...useSettingsStore.getState().settings, default_currency: 'EUR' } }) diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index edc2723b3..d0e99bb18 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -11,6 +11,7 @@ import { budgetApi } from '../../api/client' import { useExchangeRates } from '../../hooks/useExchangeRates' import { useIsMobile } from '../../hooks/useIsMobile' import { formatMoney, currencyDecimals, currencyLocale, localizeAmountInput } from '../../utils/formatters' +import { downloadBlob } from '../../utils/fileDownload' import Modal from '../shared/Modal' import CustomSelect from '../shared/CustomSelect' import { CustomDatePicker } from '../shared/CustomDateTimePicker' @@ -319,13 +320,8 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps const bom = '' const blob = new Blob([bom + rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url const safeName = (trip?.title || 'trip').replace(/[^a-zA-Z0-9À-ɏ _-]/g, '').trim() - a.download = `costs-${safeName}.csv` - a.click() - URL.revokeObjectURL(url) + downloadBlob(blob, `costs-${safeName}.csv`) } // ── small presentational helpers ──────────────────────────────────────── @@ -763,7 +759,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
    {fmt(baseTotal(e))}
    - {!isUnfinished && (e.members || []).length > 0 && Math.abs(net) > 0.01 && ( + {!unfinished && (e.members || []).length > 0 && Math.abs(net) > 0.01 && (
    0 ? '#16a34a' : '#dc2626' }}> {net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })}
    diff --git a/client/src/components/Collab/CollabChatMessages.tsx b/client/src/components/Collab/CollabChatMessages.tsx index 1fe7b658a..b335c0bef 100644 --- a/client/src/components/Collab/CollabChatMessages.tsx +++ b/client/src/components/Collab/CollabChatMessages.tsx @@ -218,12 +218,9 @@ export function ChatMessages(props: any) { borderRadius: 99, background: 'var(--bg-card)', boxShadow: '0 1px 6px rgba(0,0,0,0.12)', border: '1px solid var(--border-faint)', }}> - {msg.reactions.map(r => { - const myReaction = r.users.some(u => String(u.user_id) === String(currentUser.id)) - return ( - { if (canEdit) handleReact(msg.id, r.emoji) }} /> - ) - })} + {msg.reactions.map(r => ( + { if (canEdit) handleReact(msg.id, r.emoji) }} /> + ))}
    )} diff --git a/client/src/components/Collab/CollabChatSmall.test.tsx b/client/src/components/Collab/CollabChatSmall.test.tsx index 5087f171a..43269b718 100644 --- a/client/src/components/Collab/CollabChatSmall.test.tsx +++ b/client/src/components/Collab/CollabChatSmall.test.tsx @@ -1,4 +1,4 @@ -// FE-W4CCS-001 to FE-W4CCS-014 +// FE-W4CCS-001 to FE-W4CCS-016 import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render' import type { ChatReaction } from './CollabChat.types' @@ -155,4 +155,33 @@ describe('WebsiteThumbnail', () => { await waitFor(() => expect(linkPreview).toHaveBeenCalled()) expect(screen.getByRole('link')).toHaveAttribute('title', 'not a url') }) + + it('FE-W4CCS-015: caches per trip, because the preview endpoint is trip-scoped', async () => { + linkPreview.mockResolvedValue({ title: 'Trip 4', image: null }) + const shared = 'https://example.com/shared' + const first = render() + await waitFor(() => expect(screen.getByRole('link')).toHaveAttribute('title', 'Trip 4')) + first.unmount() + + linkPreview.mockResolvedValue({ title: 'Trip 9', image: null }) + render() + + await waitFor(() => expect(screen.getByRole('link')).toHaveAttribute('title', 'Trip 9')) + expect(linkPreview).toHaveBeenCalledTimes(2) + expect(linkPreview).toHaveBeenLastCalledWith(9, shared) + }) + + it('FE-W4CCS-016: a broken OG image does not poison the next url', async () => { + linkPreview.mockResolvedValue({ title: 'A', image: 'https://cdn.example/a.png' }) + const { container, rerender } = render() + await waitFor(() => expect(container.querySelector('img')).not.toBeNull()) + fireEvent.error(container.querySelector('img')!) + expect(container.querySelector('img')).toBeNull() + + linkPreview.mockResolvedValue({ title: 'B', image: 'https://cdn.example/b.png' }) + rerender() + + await waitFor(() => expect(container.querySelector('img')).not.toBeNull()) + expect(container.querySelector('img')).toHaveAttribute('src', 'https://cdn.example/b.png') + }) }) diff --git a/client/src/components/Collab/CollabNotes.test.tsx b/client/src/components/Collab/CollabNotes.test.tsx index 0bb6129f2..618699e98 100644 --- a/client/src/components/Collab/CollabNotes.test.tsx +++ b/client/src/components/Collab/CollabNotes.test.tsx @@ -1270,7 +1270,7 @@ describe('CollabNotes', () => { }); }); -// FE-W5CNT-001 to FE-W5CNT-028 +// FE-W5CNT-001 to FE-W5CNT-029 // Fills in the load/error/attachment/category branches of useCollabNotes and the // view modal that the smoke tests above do not reach. @@ -1802,4 +1802,21 @@ describe('CollabNotes details', () => { await screen.findByText('Newer note'); expect(screen.getByText('Older note')).toBeInTheDocument(); }); + + it('FE-W5CNT-029: a failing delete reports an error and keeps the note in the list', async () => { + const user = userEvent.setup(); + serveNotes({ notes: [buildNote({ id: 30, title: 'Stubborn note' })] }); + server.use( + http.delete('/api/trips/1/collab/notes/30', () => new HttpResponse(null, { status: 500 })), + ); + render(); + await screen.findByText('Stubborn note'); + + await user.click(screen.getByTitle('Delete')); + const dialog = (await screen.findByText('Delete note?')).closest('div.trek-modal-enter') as HTMLElement; + await user.click(within(dialog).getByRole('button', { name: 'Delete' })); + + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Error', 'error', undefined)); + expect(screen.getByText('Stubborn note')).toBeInTheDocument(); + }); }); diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index 04deb0a85..b5e8fb0e2 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -271,7 +271,7 @@ function CollabNotesHeader({ t, canEdit, setShowSettings, setShowNewModal }: Not {t('collab.notes.title')}
    - {canEdit &&
    diff --git a/client/src/components/Collections/CollectionPicker.tsx b/client/src/components/Collections/CollectionPicker.tsx index 5626ed87c..5cc071c82 100644 --- a/client/src/components/Collections/CollectionPicker.tsx +++ b/client/src/components/Collections/CollectionPicker.tsx @@ -31,6 +31,28 @@ function distanceTo(p: CollectionPlace, center: { lat: number; lng: number }): n interface Opt { key: string | number; label: string; icon?: React.ReactNode; count?: number } +/** Detail requests fired at once while building the union — keeps a user with + * many lists from opening the modal with a burst of parallel requests. */ +const DETAIL_BATCH = 4 + +/** Union of every list's places, in list order, without duplicates. */ +async function loadSavedPlaces(ids: number[]): Promise { + const merged: CollectionPlace[] = [] + const seen = new Set() + for (let i = 0; i < ids.length; i += DETAIL_BATCH) { + const batch = await Promise.all(ids.slice(i, i + DETAIL_BATCH).map(id => collectionsApi.get(id).catch(() => null))) + for (const d of batch) { + if (!d) continue + for (const p of d.places) { + if (seen.has(p.id)) continue + seen.add(p.id) + merged.push(p) + } + } + } + return merged +} + /** Compact click-away dropdown (Tailwind — this panel lives outside .trek-dash). */ function FilterDropdown({ current, options, onSelect, lead }: { current: string | number @@ -93,13 +115,8 @@ export default function CollectionPicker({ bias, onSelect, t }: CollectionPicker setLoading(true) collectionsApi.list() .then(async (res) => { - const detail = await Promise.all(res.collections.map(c => collectionsApi.get(c.id).catch(() => null))) + const merged = await loadSavedPlaces(res.collections.map(c => c.id)) if (cancelled) return - const merged: CollectionPlace[] = [] - for (const d of detail) { - if (!d) continue - for (const p of d.places) merged.push(p) - } setLists(res.collections.map(c => ({ id: c.id, name: c.name, color: c.color ?? null }))) setPlaces(merged) }) diff --git a/client/src/components/Collections/CollectionPlaceDetail.test.tsx b/client/src/components/Collections/CollectionPlaceDetail.test.tsx index 440097c64..9fd473998 100644 --- a/client/src/components/Collections/CollectionPlaceDetail.test.tsx +++ b/client/src/components/Collections/CollectionPlaceDetail.test.tsx @@ -233,7 +233,8 @@ describe('CollectionPlaceDetail', () => { const input = document.querySelector('input[type="file"]') as HTMLInputElement; fireEvent.change(input, { target: { files: [new File(['x'], 'cover.png', { type: 'image/png' })] } }); - await waitFor(() => expect(addToast).toHaveBeenCalledWith('Upload rejected', 'error', undefined)); + // A rejection without a server payload falls back to the localized message. + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Could not upload image', 'error', undefined)); // The spinner is gone again, so the control accepts a retry. await waitFor(() => expect(document.querySelector('.animate-spin')).toBeNull()); }); diff --git a/client/src/components/Collections/CollectionPlaceDetail.tsx b/client/src/components/Collections/CollectionPlaceDetail.tsx index 0d8e3612f..42a7991da 100644 --- a/client/src/components/Collections/CollectionPlaceDetail.tsx +++ b/client/src/components/Collections/CollectionPlaceDetail.tsx @@ -15,7 +15,7 @@ import { useToast } from '../shared/Toast' import { Tooltip } from '../shared/Tooltip' import PlaceRating from '../shared/StarRating' import { normalizeImageFile } from '../../utils/convertHeic' -import { getApiErrorMessage } from '../../types' +import { getApiErrorMessage } from '../../utils/apiError' function linkHost(url: string): string { try { return new URL(url).hostname.replace(/^www\./, '') } catch { return url } diff --git a/client/src/components/Collections/ListEditorModal.test.tsx b/client/src/components/Collections/ListEditorModal.test.tsx index 7b965a37e..bac602ebd 100644 --- a/client/src/components/Collections/ListEditorModal.test.tsx +++ b/client/src/components/Collections/ListEditorModal.test.tsx @@ -322,7 +322,8 @@ describe('ListEditorModal', () => { const { props } = setup() fireEvent.change(screen.getByPlaceholderText('e.g. Tokyo 2025'), { target: { value: 'Oslo' } }) fireEvent.click(screen.getByRole('button', { name: 'Create' })) - await waitFor(() => expect(addToast).toHaveBeenCalledWith('boom', 'error', undefined)) + // A plain failure has no server-provided message, so the translated fallback shows. + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Error', 'error', undefined)) expect(props.onClose).not.toHaveBeenCalled() // The save button releases again so the user can retry. await waitFor(() => expect(screen.getByRole('button', { name: 'Create' })).not.toBeDisabled()) diff --git a/client/src/components/Collections/ListEditorModal.tsx b/client/src/components/Collections/ListEditorModal.tsx index 2b0f5cf86..9b0609cb1 100644 --- a/client/src/components/Collections/ListEditorModal.tsx +++ b/client/src/components/Collections/ListEditorModal.tsx @@ -4,7 +4,7 @@ import Modal from '../shared/Modal' import { useCollectionStore } from '../../store/collectionStore' import { useToast } from '../shared/Toast' import { tripsApi } from '../../api/client' -import { getApiErrorMessage } from '../../types' +import { getApiErrorMessage } from '../../utils/apiError' import { normalizeLinkUrl } from '../../pages/collections/collectionsModel' import type { TranslationFn } from '../../types' import type { Collection, CollectionLink } from '@trek/shared' diff --git a/client/src/components/Collections/SaveToCollectionModal.test.tsx b/client/src/components/Collections/SaveToCollectionModal.test.tsx index 61e6637e5..e3487b8cc 100644 --- a/client/src/components/Collections/SaveToCollectionModal.test.tsx +++ b/client/src/components/Collections/SaveToCollectionModal.test.tsx @@ -219,7 +219,7 @@ describe('SaveToCollectionModal', () => { expect(useSaveToCollectionStore.getState().version).toBe(0); }); - it('FE-COMP-SAVETOCOL-013: a second click on the busy row is ignored while the save runs', async () => { + it('FE-COMP-SAVETOCOL-013: every row locks while a save runs', async () => { let resolve!: (v: CollectionSaveResult) => void; const save = vi.spyOn(collectionsApi, 'savePlace').mockReturnValue(new Promise(r => { resolve = r; })); openFor(); @@ -228,7 +228,9 @@ describe('SaveToCollectionModal', () => { const wishlist = await screen.findByRole('button', { name: /Wishlist/ }); fireEvent.click(wishlist); expect(wishlist).toBeDisabled(); - fireEvent.click(screen.getByRole('button', { name: /Favorites/ })); + const favorites = screen.getByRole('button', { name: /Favorites/ }); + expect(favorites).toBeDisabled(); + fireEvent.click(favorites); expect(save).toHaveBeenCalledTimes(1); resolve({}); diff --git a/client/src/components/Collections/SaveToCollectionModal.tsx b/client/src/components/Collections/SaveToCollectionModal.tsx index 521fd84d4..77d3bc476 100644 --- a/client/src/components/Collections/SaveToCollectionModal.tsx +++ b/client/src/components/Collections/SaveToCollectionModal.tsx @@ -169,7 +169,7 @@ export default function SaveToCollectionModal(): React.ReactElement | null { key={list.id} type="button" onClick={() => handleToggle(list)} - disabled={busy} + disabled={busyId != null} className={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-left transition-colors disabled:opacity-60 ${saved ? 'border-accent bg-accent-subtle' : 'border-edge bg-surface-card hover:bg-surface-hover'}`} > diff --git a/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx b/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx index 4f7f23f8b..f588d579c 100644 --- a/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx +++ b/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx @@ -98,7 +98,7 @@ describe('SaveTripPlacesToListModal', () => { vi.spyOn(collectionsApi, 'list').mockResolvedValue(listResponse(many)); renderModal(); - const search = await screen.findByPlaceholderText('Search trips'); + const search = await screen.findByPlaceholderText('Search lists'); fireEvent.change(search, { target: { value: 'list 4' } }); expect(screen.getByText('List 4')).toBeInTheDocument(); expect(screen.queryByText('List 5')).not.toBeInTheDocument(); @@ -110,7 +110,7 @@ describe('SaveTripPlacesToListModal', () => { it('FE-COMP-SAVETRIPPL-007: five lists or fewer need no search box', async () => { renderModal(); await screen.findByText('Favorites'); - expect(screen.queryByPlaceholderText('Search trips')).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('Search lists')).not.toBeInTheDocument(); }); it('FE-COMP-SAVETRIPPL-008: picking a list copies every selected place, reports it and closes', async () => { diff --git a/client/src/components/Collections/SaveTripPlacesToListModal.tsx b/client/src/components/Collections/SaveTripPlacesToListModal.tsx index 120f18f9c..3063d0d8e 100644 --- a/client/src/components/Collections/SaveTripPlacesToListModal.tsx +++ b/client/src/components/Collections/SaveTripPlacesToListModal.tsx @@ -77,7 +77,7 @@ export default function SaveTripPlacesToListModal({ isOpen, tripId, placeIds, on autoFocus value={search} onChange={e => setSearch(e.target.value)} - placeholder={t('collections.copyToTripSearch')} + placeholder={t('collections.searchLists')} className="w-full pl-8 pr-3 py-2 rounded-lg border border-edge bg-surface-input text-content text-[13px] outline-none focus:border-accent" />
diff --git a/client/src/components/Collections/ShareCollectionModal.tsx b/client/src/components/Collections/ShareCollectionModal.tsx index b9ff7868e..5ade54945 100644 --- a/client/src/components/Collections/ShareCollectionModal.tsx +++ b/client/src/components/Collections/ShareCollectionModal.tsx @@ -114,6 +114,7 @@ export default function ShareCollectionModal({ } const handleSetRole = async (userId: number, role: CollectionRole) => { + if (settingRoleId != null) return setSettingRoleId(userId) try { await setMemberRole(collectionId, userId, role) diff --git a/client/src/components/Files/FileManagerImageLightbox.test.tsx b/client/src/components/Files/FileManagerImageLightbox.test.tsx index ce25174db..e49ff1f5f 100644 --- a/client/src/components/Files/FileManagerImageLightbox.test.tsx +++ b/client/src/components/Files/FileManagerImageLightbox.test.tsx @@ -1,4 +1,4 @@ -// FE-W4LBX-001 to FE-W4LBX-017 +// FE-W4LBX-001 to FE-W4LBX-019 import { describe, it, expect, vi, beforeEach } from 'vitest' import type { TripFile } from '../../types' import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render' @@ -208,4 +208,41 @@ describe('ImageLightbox', () => { expect(onClose).not.toHaveBeenCalled() }) + + it('FE-W4LBX-018: Escape reaches the current onClose after a re-render', () => { + const first = vi.fn() + const second = vi.fn() + const { rerender } = render() + + rerender() + fireEvent.keyDown(window, { key: 'Escape' }) + + expect(second).toHaveBeenCalledOnce() + expect(first).not.toHaveBeenCalled() + }) + + it('FE-W4LBX-019: the strip mints thumbnail tokens only for thumbs in view', async () => { + // Every thumbnail costs its own one-shot token, so an off-screen thumb must + // stay quiet. The default observer stub never intersects. + const { unmount } = render( {}} />) + await waitFor(() => expect(getAuthUrl).toHaveBeenCalledWith('/f/a.jpg', 'download')) + expect(getAuthUrl).toHaveBeenCalledTimes(1) + unmount() + + getAuthUrl.mockClear() + const original = globalThis.IntersectionObserver + globalThis.IntersectionObserver = class { + constructor(private cb: IntersectionObserverCallback) {} + observe() { this.cb([{ isIntersecting: true } as IntersectionObserverEntry], this as unknown as IntersectionObserver) } + disconnect() {} + unobserve() {} + takeRecords() { return [] } + } as unknown as typeof IntersectionObserver + try { + render( {}} />) + await waitFor(() => expect(getAuthUrl).toHaveBeenCalledWith('/f/c.jpg', 'download')) + } finally { + globalThis.IntersectionObserver = original + } + }) }) diff --git a/client/src/components/Files/FileManagerImageLightbox.tsx b/client/src/components/Files/FileManagerImageLightbox.tsx index 5492d412c..0afaa127a 100644 --- a/client/src/components/Files/FileManagerImageLightbox.tsx +++ b/client/src/components/Files/FileManagerImageLightbox.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { ExternalLink, Download, X, ChevronLeft, ChevronRight, Play } from 'lucide-react' import { useTranslation } from '../../i18n' import type { TripFile } from '../../types' @@ -41,7 +41,7 @@ export function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxPro } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) - }, []) + }, [onClose]) if (!file) return null @@ -128,11 +128,30 @@ export function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxPro function ThumbImg({ file, active, onClick }: { file: TripFile & { url: string }; active: boolean; onClick: () => void }) { const fileIsVideo = isVideo(file.mime_type) const [src, setSrc] = useState('') + const [visible, setVisible] = useState(false) + const ref = useRef(null) + + // Each thumbnail costs its own one-shot download token, so the strip only + // mints them for the thumbs that actually scroll into view. + useEffect(() => { + const el = ref.current + if (!el || typeof IntersectionObserver !== 'function') { setVisible(true); return } + const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVisible(true); io.disconnect() } }, { rootMargin: '200px' }) + io.observe(el) + return () => io.disconnect() + }, []) + // Videos have no stored thumbnail and can't render as an ; show a play // placeholder and don't mint a download token for them (#823). - useEffect(() => { if (!fileIsVideo) getAuthUrl(file.url, 'download').then(setSrc) }, [file.url, fileIsVideo]) + useEffect(() => { + if (!visible || fileIsVideo) return + let current = true + getAuthUrl(file.url, 'download').then(u => { if (current) setSrc(u) }) + return () => { current = false } + }, [file.url, fileIsVideo, visible]) + return ( -
`, + ">${svg}${labelHtml}
`, iconSize: [estWidth, 22], iconAnchor: [estWidth / 2, 11], popupAnchor: [0, -11], @@ -135,182 +133,18 @@ interface TransportItem { type: TransportType arcs: [number, number][][] transitSegs: TransitMapSegment[] - primaryArc: [number, number][] - fallback: [number, number] + // Route ("VIE → LHR") and duration/distance line. Computed on every update but + // not drawn since the stats badge was dropped; computeDuration still guards the + // non-finite date that used to blank the trip (#1620). mainLabel: string | null subLabel: string | null } -function buildStatsHtml(color: string, mainLabel: string | null, subLabel: string | null): { html: string; width: number; height: number } { - const estWidth = Math.max( - mainLabel ? mainLabel.length * 6.5 : 0, - subLabel ? subLabel.length * 5.5 : 0, - ) + 22 - const hasBoth = !!mainLabel && !!subLabel - const height = hasBoth ? 36 : 22 - const main = mainLabel ? `${escapeHtml(mainLabel)}` : '' - const sub = subLabel ? `${escapeHtml(subLabel)}` : '' - const html = `
${main}${sub}
` - return { html, width: estWidth, height } -} - -function StatsLabel({ item }: { item: TransportItem }) { - const map = useMap() - const markerRef = useRef(null) - const innerRef = useRef(null) - - const arc = item.primaryArc - const color = TYPE_META[item.type].color - - const { html, width, height } = useMemo(() => buildStatsHtml(color, item.mainLabel, item.subLabel), [color, item.mainLabel, item.subLabel]) - const buffer = AIRPORT_BADGE_HALF_PX + width / 2 + BADGE_GAP_PX - - const compute = () => { - if (arc.length < 2) return null - const size = map.getSize() - const pts = arc.map(p => map.latLngToContainerPoint(p as L.LatLngTuple)) - const cum: number[] = [0] - let total = 0 - for (let i = 1; i < pts.length; i++) { - total += pts[i].distanceTo(pts[i - 1]) - cum.push(total) - } - if (total <= 0) return null - - const fromPx = map.latLngToContainerPoint([item.from.lat, item.from.lng]) - const toPx = map.latLngToContainerPoint([item.to.lat, item.to.lng]) - - const isIn = (p: L.Point) => { - if (p.x < -40 || p.x > size.x + 40 || p.y < -40 || p.y > size.y + 40) return false - if (p.distanceTo(fromPx) < buffer) return false - if (p.distanceTo(toPx) < buffer) return false - return true - } - - let firstIdx = -1 - let lastIdx = -1 - for (let i = 0; i < pts.length; i++) { - if (isIn(pts[i])) { - if (firstIdx < 0) firstIdx = i - lastIdx = i - } - } - if (firstIdx < 0) { - const target = total / 2 - let sIdx = 0 - while (sIdx < cum.length - 2 && cum[sIdx + 1] < target) sIdx++ - const span = cum[sIdx + 1] - cum[sIdx] - const tm = span > 0 ? (target - cum[sIdx]) / span : 0 - const pA = pts[sIdx] - const pB = pts[sIdx + 1] - const mx = pA.x + (pB.x - pA.x) * tm - const my = pA.y + (pB.y - pA.y) * tm - const latlng = map.containerPointToLatLng([mx, my]) - let angle = Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - return { point: [latlng.lat, latlng.lng] as [number, number], angle } - } - - const bisectFraction = (a: L.Point, b: L.Point) => { - let lo = 0, hi = 1 - for (let k = 0; k < 10; k++) { - const mid = (lo + hi) / 2 - const mp = L.point(a.x + (b.x - a.x) * mid, a.y + (b.y - a.y) * mid) - if (isIn(mp)) hi = mid - else lo = mid - } - return (lo + hi) / 2 - } - - let lowCum = cum[firstIdx] - if (firstIdx > 0) { - const t = bisectFraction(pts[firstIdx - 1], pts[firstIdx]) - lowCum = cum[firstIdx - 1] + (cum[firstIdx] - cum[firstIdx - 1]) * t - } - let highCum = cum[lastIdx] - if (lastIdx < pts.length - 1) { - const t = bisectFraction(pts[lastIdx + 1], pts[lastIdx]) - highCum = cum[lastIdx] + (cum[lastIdx + 1] - cum[lastIdx]) * (1 - t) - } - - const targetLen = (lowCum + highCum) / 2 - - let segIdx = 0 - while (segIdx < cum.length - 2 && cum[segIdx + 1] < targetLen) segIdx++ - const segSpan = cum[segIdx + 1] - cum[segIdx] - const t = segSpan > 0 ? (targetLen - cum[segIdx]) / segSpan : 0 - const pA = pts[segIdx] - const pB = pts[segIdx + 1] - const px = pA.x + (pB.x - pA.x) * t - const py = pA.y + (pB.y - pA.y) * t - const latlng = map.containerPointToLatLng([px, py]) - - let angle = Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - - return { point: [latlng.lat, latlng.lng] as [number, number], angle } - } - - const apply = () => { - const pose = compute() - const marker = markerRef.current - if (!marker) return - const el = marker.getElement() as HTMLElement | null - if (!pose) { - if (el) el.style.display = 'none' - return - } - if (el) el.style.display = '' - marker.setLatLng(pose.point as L.LatLngTuple) - if (!innerRef.current && el) innerRef.current = el.querySelector('.trek-stats-inner') as HTMLElement | null - if (innerRef.current) innerRef.current.style.transform = `rotate(${pose.angle}deg)` - } - - useEffect(() => { - const icon = L.divIcon({ - className: 'trek-endpoint-stats', - html, - iconSize: [width, height], - iconAnchor: [width / 2, height / 2], - }) - const marker = L.marker([0, 0], { icon, pane: ENDPOINT_PANE, interactive: false, keyboard: false }) - marker.addTo(map) - markerRef.current = marker - innerRef.current = null - apply() - return () => { - marker.remove() - markerRef.current = null - innerRef.current = null - } - }, [map, html, width, height]) - - useMapEvents({ - move: apply, - zoom: apply, - viewreset: apply, - resize: apply, - }) - - return null -} - interface Props { reservations: Reservation[] showConnections: boolean + // Accepted for call-site compatibility only: the floating route/duration badge + // on the arc is no longer drawn, so nothing is gated on this. showStats: boolean onEndpointClick?: (reservationId: number) => void // Real road-network geometry for car/bus/taxi/bicycle bookings, keyed by @@ -318,7 +152,7 @@ interface Props { roadRoutes?: Map } -export default function ReservationOverlay({ reservations, showConnections, showStats, onEndpointClick, roadRoutes }: Props) { +export default function ReservationOverlay({ reservations, showConnections, onEndpointClick, roadRoutes }: Props) { useEndpointPane() const map = useMap() const [zoom, setZoom] = useState(() => map.getZoom()) @@ -354,12 +188,6 @@ export default function ReservationOverlay({ reservations, showConnections, show arcs.push(...segArcs) distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) } - const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) - const primaryArc = arcs[primaryIdx] ?? [] - const fallback: [number, number] = primaryArc.length > 0 - ? (primaryArc[Math.floor(primaryArc.length / 2)] ?? [(from.lat + to.lat) / 2, (from.lng + to.lng) / 2]) - : [(from.lat + to.lat) / 2, (from.lng + to.lng) / 2] - const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) const distance = `${Math.round(distanceKm)} km` // Show the full route (FRA → BER → HND) when every waypoint has a code. @@ -369,7 +197,7 @@ export default function ReservationOverlay({ reservations, showConnections, show const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, waypoints, type, arcs, transitSegs: type === 'transit' ? getTransitMapSegments(r) : [], primaryArc, fallback, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, transitSegs: type === 'transit' ? getTransitMapSegments(r) : [], mainLabel, subLabel }) } return out }, [reservations]) diff --git a/client/src/components/Map/placePhoto.ts b/client/src/components/Map/placePhoto.ts index a57294624..71874b82c 100644 --- a/client/src/components/Map/placePhoto.ts +++ b/client/src/components/Map/placePhoto.ts @@ -8,3 +8,21 @@ export function isCustomPlaceImage(url: string | null | undefined): boolean { return !!url && (url.startsWith('/uploads/') || url.startsWith('data:')) } + +/** + * Cache key under which a place's auto-fetched thumbnail is stored: the provider + * id when there is one, otherwise the coordinates. A place with neither has no + * identity to cache under, so it gets the empty string — callers skip it rather + * than filing every coordinate-less place under one shared entry. + */ +export function photoCacheKey(place: { + google_place_id?: string | null + osm_id?: string | null + lat?: number | null + lng?: number | null +}): string { + if (place.google_place_id) return place.google_place_id + if (place.osm_id) return place.osm_id + if (place.lat == null || place.lng == null) return '' + return `${place.lat},${place.lng}` +} diff --git a/client/src/components/Map/reservationsMapbox.test.ts b/client/src/components/Map/reservationsMapbox.test.ts index ce02a60bb..2a7a9f39f 100644 --- a/client/src/components/Map/reservationsMapbox.test.ts +++ b/client/src/components/Map/reservationsMapbox.test.ts @@ -264,7 +264,6 @@ describe('ReservationMapboxOverlay lifecycle', () => { expect(glMarkers.every(m => m.remove.mock.calls.length > 0)).toBe(true) expect(map.listeners('zoomend')).toBe(0) expect(map.listeners('moveend')).toBe(0) - expect(map.listeners('render')).toBe(0) expect(map.removeLayer).toHaveBeenCalledWith(RESERVATION_LINE_LAYER_ID) expect(map.removeSource).toHaveBeenCalledWith(RESERVATION_SOURCE_ID) }) @@ -287,20 +286,18 @@ describe('ReservationMapboxOverlay lifecycle', () => { expect(features(map)).toHaveLength(1) }) - it('FE-COMP-RESGL-006: the redraw and rotation handlers are inert once the overlay is destroyed', () => { + it('FE-COMP-RESGL-006: the redraw handler is inert once the overlay is destroyed', () => { const map = freshMap() const overlay = attach(map) const handlerFor = (event: string) => map.on.mock.calls.find(c => c[0] === event)?.[1] as Handler const rerender = handlerFor('moveend') - const rotate = handlerFor('render') overlay.update([booking()], opts) const drawsBefore = map._sources.get(RESERVATION_SOURCE_ID)!.setData.mock.calls.length overlay.destroy() rerender() - rotate() expect(map._sources.get(RESERVATION_SOURCE_ID)).toBeUndefined() expect(drawsBefore).toBeGreaterThan(0) }) @@ -700,7 +697,7 @@ describe('ReservationMapboxOverlay transit styling', () => { }) describe('ReservationMapboxOverlay frame handler', () => { - it('FE-COMP-RESGL-042: a paint frame rotates only — it does not redraw the overlay', () => { + it('FE-COMP-RESGL-042: the overlay does not subscribe to paint frames — only camera events redraw it', () => { const map = freshMap() const overlay = attach(map) overlay.update([booking()], opts) @@ -708,6 +705,7 @@ describe('ReservationMapboxOverlay frame handler', () => { map.fire('render') + expect(map.listeners('render')).toBe(0) expect(map._sources.get(RESERVATION_SOURCE_ID)!.setData.mock.calls).toHaveLength(drawsBefore) expect(glMarkers).toHaveLength(2) expect(glMarkers.every(m => m.remove.mock.calls.length === 0)).toBe(true) diff --git a/client/src/components/Map/reservationsMapbox.ts b/client/src/components/Map/reservationsMapbox.ts index bd5c849ee..91b73e960 100644 --- a/client/src/components/Map/reservationsMapbox.ts +++ b/client/src/components/Map/reservationsMapbox.ts @@ -102,7 +102,9 @@ interface TransportItem { waypoints: ReservationEndpoint[] type: TransportType arcs: [number, number][][] - primaryArc: [number, number][] + // Route ("VIE → LHR") and duration/distance line. Computed on every update but + // not drawn since the stats badge was dropped; computeDuration still guards the + // non-finite date that used to blank the trip (#1620). mainLabel: string | null subLabel: string | null } @@ -136,8 +138,6 @@ function buildItems(reservations: Reservation[]): TransportItem[] { arcs.push(...segArcs) distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) } - const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) - const primaryArc = arcs[primaryIdx] ?? [] const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) const distance = `${Math.round(distanceKm)} km` const mainLabel = waypoints.every(w => w.code) @@ -145,7 +145,7 @@ function buildItems(reservations: Reservation[]): TransportItem[] { : (from.code && to.code ? `${from.code} → ${to.code}` : null) const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, waypoints, type, arcs, primaryArc, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, mainLabel, subLabel }) } return out } @@ -165,32 +165,11 @@ function endpointMarkerHtml(type: TransportType, label: string | null): string { ">${svg}${labelHtml}` } -function buildStatsHtml(mainLabel: string | null, subLabel: string | null): { html: string; width: number; height: number } { - const estWidth = Math.max( - mainLabel ? mainLabel.length * 6.5 : 0, - subLabel ? subLabel.length * 5.5 : 0, - ) + 22 - const hasBoth = !!mainLabel && !!subLabel - const height = hasBoth ? 36 : 22 - const main = mainLabel ? `${escapeHtml(mainLabel)}` : '' - const sub = subLabel ? `${escapeHtml(subLabel)}` : '' - const html = `
${main}${sub}
` - return { html, width: estWidth, height } -} - // ── overlay manager ────────────────────────────────────────────────────── export interface ReservationOverlayOptions { showConnections: boolean + // Accepted for call-site compatibility only: the floating route/duration badge + // on the arc is no longer drawn, so nothing is gated on this. showStats: boolean showEndpointLabels: boolean onEndpointClick?: (reservationId: number) => void @@ -212,7 +191,6 @@ export class ReservationMapboxOverlay { private opts: ReservationOverlayOptions private MarkerCtor: MarkerConstructor private endpointMarkers: GlMarker[] = [] - private statsMarkers: { marker: GlMarker; arc: [number, number][] }[] = [] private rerender: () => void private destroyed = false @@ -224,7 +202,6 @@ export class ReservationMapboxOverlay { this.setupLayer() map.on('zoomend', this.rerender) map.on('moveend', this.rerender) - map.on('render', this.updateStatsRotation) } update(reservations: Reservation[], opts: ReservationOverlayOptions, roadRoutes?: Map) { @@ -238,11 +215,8 @@ export class ReservationMapboxOverlay { this.destroyed = true this.map.off('zoomend', this.rerender) this.map.off('moveend', this.rerender) - this.map.off('render', this.updateStatsRotation) this.endpointMarkers.forEach(m => m.remove()) this.endpointMarkers = [] - this.statsMarkers.forEach(s => s.marker.remove()) - this.statsMarkers = [] try { if (this.map.getLayer(RESERVATION_LINE_LAYER_ID)) this.map.removeLayer(RESERVATION_LINE_LAYER_ID) if (this.map.getSource(RESERVATION_SOURCE_ID)) this.map.removeSource(RESERVATION_SOURCE_ID) @@ -386,34 +360,7 @@ export class ReservationMapboxOverlay { } } - // Stats badge removed — the floating route/duration label on the arc is no - // longer drawn; only the connection line and the airport markers remain. - this.statsMarkers.forEach(s => s.marker.remove()) - this.statsMarkers = [] - } - - // Match the Leaflet overlay's "rotate the label along the arc" look. - // We pick a short segment straddling the arc midpoint, measure the - // screen angle between those two projected points, and clamp it to - // [-90°, 90°] so text never renders upside-down. - private updateStatsRotation = () => { - if (this.destroyed) return - for (const entry of this.statsMarkers) { - const { marker, arc } = entry - if (arc.length < 2) continue - const midIdx = Math.floor(arc.length / 2) - const a = arc[Math.max(0, midIdx - 2)]! - const b = arc[Math.min(arc.length - 1, midIdx + 2)]! - try { - const pa = this.map.project([a[1], a[0]]) - const pb = this.map.project([b[1], b[0]]) - let angle = Math.atan2(pb.y - pa.y, pb.x - pa.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - const el = marker.getElement() - const inner = el.querySelector('.trek-stats-inner') as HTMLElement | null - if (inner) inner.style.transform = `rotate(${angle}deg)` - } catch { /* map not ready / projection failure */ } - } + // No stats badge: the floating route/duration label on the arc was removed, + // so a rendered overlay is the connection lines plus the airport markers. } } diff --git a/client/src/components/Map/usePoiExplore.test.tsx b/client/src/components/Map/usePoiExplore.test.tsx index 741ed9169..66d0346d5 100644 --- a/client/src/components/Map/usePoiExplore.test.tsx +++ b/client/src/components/Map/usePoiExplore.test.tsx @@ -1,4 +1,4 @@ -// FE-COMP-POIEXPLORE-001 to FE-COMP-POIEXPLORE-016 +// FE-COMP-POIEXPLORE-001 to FE-COMP-POIEXPLORE-018 import React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { act, renderHook, waitFor } from '@testing-library/react' @@ -240,4 +240,34 @@ describe('usePoiExplore', () => { expect(result.current.errorKeys.size).toBe(0) expect(result.current.pois[0].osm_id).toBe('node/9') }) + + it('FE-COMP-POIEXPLORE-017: the abandoned category stops loading when the selection moves on', async () => { + const first = deferred() + pois.mockReturnValueOnce(first.promise) + const { result } = setup() + act(() => { result.current.onViewportChange(BBOX) }) + act(() => { result.current.toggle('cafe') }) + expect(result.current.loadingKeys.has('cafe')).toBe(true) + + pois.mockResolvedValue(response([poi({ osm_id: 'node/9', category: 'bar' })])) + act(() => { result.current.toggle('bar') }) + + await act(async () => { first.reject(Object.assign(new Error('canceled'), { name: 'CanceledError' })) }) + + await waitFor(() => expect(result.current.loadingKeys.has('cafe')).toBe(false)) + expect(result.current.loadingKeys.has('bar')).toBe(false) + }) + + it('FE-COMP-POIEXPLORE-018: turning the category off stops its spinner as well', async () => { + const d = deferred() + pois.mockReturnValue(d.promise) + const { result } = setup() + act(() => { result.current.onViewportChange(BBOX) }) + act(() => { result.current.toggle('cafe') }) + act(() => { result.current.toggle('cafe') }) + + await act(async () => { d.reject(Object.assign(new Error('canceled'), { name: 'CanceledError' })) }) + + await waitFor(() => expect(result.current.loadingKeys.size).toBe(0)) + }) }) diff --git a/client/src/components/Map/usePoiExplore.ts b/client/src/components/Map/usePoiExplore.ts index 710adc757..335f3afaa 100644 --- a/client/src/components/Map/usePoiExplore.ts +++ b/client/src/components/Map/usePoiExplore.ts @@ -74,6 +74,11 @@ export function usePoiExplore() { if (abortRef.current[key] === ctrl) { setLoading(key, false) delete abortRef.current[key] + } else if (!abortRef.current[key]) { + // Cancelled with nothing taking over (toggle switched the category, or + // turned it off) — no later request will clear this key, so do it here + // instead of leaving the pill spinning forever. + setLoading(key, false) } } }, [setLoading, setError, locale]) diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index 3c4350e8f..ce03819e9 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -899,4 +899,16 @@ describe('downloadTripPDF defaults', () => { expect(endSection).toContain('reservations.span.end: Overnight Bus') expect(endSection).not.toContain('22:00') }) + + it('FE-W5PDF-027: an export without a locale still renders, with lang="en"', async () => { + await downloadTripPDF({ ...minimalArgs, locale: '' }) + + expect(srcdoc()).toContain('') + }) + + it('FE-W5PDF-028: an export without assignments renders the days as empty', async () => { + await downloadTripPDF({ ...minimalArgs, assignments: undefined as any }) + + expect(srcdoc()).toContain('Day 1') + }) }) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index c3d084a38..a39515fb6 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -146,7 +146,9 @@ interface downloadTripPDFProps { locale: string } -export async function downloadTripPDF({ trip, days, places, assignments, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) { +// `assignments` is normalised here once — every read below (and fetchPlacePhotos) +// relies on it being an object. +export async function downloadTripPDF({ trip, days, places, assignments = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) { await ensureRenderer() const loc = _locale || undefined const tr = _t || (k => k) @@ -165,7 +167,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const photoMap = await fetchPlacePhotos(assignments, places) const totalAssigned = new Set( - Object.values(assignments || {}).flatMap(a => a.map(x => x.place?.id)).filter(Boolean) + Object.values(assignments).flatMap(a => a.map(x => x.place?.id)).filter(Boolean) ).size // The PDF is a trip-scoped, shareable document, so totals stay in the trip's // own currency. Rates are resolved ONCE before any HTML is built so the cover @@ -173,7 +175,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor // entirely (offline export keeps working), and a failed fetch degrades to // per-currency breakdowns instead of mislabeled sums (#1561). const tripCur = (trip?.currency || 'EUR').toUpperCase() - const allCostEntries: MoneyEntry[] = Object.values(assignments || {}) + const allCostEntries: MoneyEntry[] = Object.values(assignments) .flatMap(a => a) .map(a => ({ amount: Number(a.place?.price) || 0, currency: a.place?.currency || tripCur })) const needsFx = allCostEntries.some(e => e.amount > 0 && e.currency.toUpperCase() !== tripCur) @@ -433,7 +435,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ` const html = ` - + diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx index 7c5f701a8..ebc2edbb2 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx @@ -229,24 +229,28 @@ describe('KategorieGruppe — bulk actions', () => { await waitFor(() => expect(puts).toEqual([1])) }) - it('FE-W5CAT-012: a throwing toggle during Check All surfaces a save error', async () => { - useTripStore.setState({ togglePackingItem: async () => { throw new Error('offline') } }) + it('FE-W5CAT-012: a failing PUT during Check All is reported by the store, once', async () => { + server.use(http.put('/api/trips/1/packing/:itemId', () => new HttpResponse(null, { status: 500 }))) + seedStore(useTripStore, { packingItems: [buildPackingItem({ id: 1, name: 'Tent', checked: 0 })] }) const { container } = setup({ items: [buildPackingItem({ id: 1, name: 'Tent', checked: 0 })] }) openCategoryMenu(container) fireEvent.click(screen.getByText('Check All')) - await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Failed to save', 'error', undefined)) + await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Error updating item', 'error', undefined)) + expect(toastSpy).toHaveBeenCalledTimes(1) }) - it('FE-W5CAT-013: a throwing toggle during Uncheck All surfaces a save error', async () => { - useTripStore.setState({ togglePackingItem: async () => { throw new Error('offline') } }) + it('FE-W5CAT-013: a failing PUT during Uncheck All is reported by the store, once', async () => { + server.use(http.put('/api/trips/1/packing/:itemId', () => new HttpResponse(null, { status: 500 }))) + seedStore(useTripStore, { packingItems: [buildPackingItem({ id: 1, name: 'Tent', checked: 1 })] }) const { container } = setup({ items: [buildPackingItem({ id: 1, name: 'Tent', checked: 1 })] }) openCategoryMenu(container) fireEvent.click(screen.getByText('Uncheck All')) - await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Failed to save', 'error', undefined)) + await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Error updating item', 'error', undefined)) + expect(toastSpy).toHaveBeenCalledTimes(1) }) it('FE-W5CAT-014: Delete List hands the whole category to the panel and closes the menu', async () => { diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx index 66145b21f..5579f0955 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx @@ -98,19 +98,17 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen catch { toast.error(t('packing.toast.renameError')) } } + // togglePackingItem rolls its own optimistic update back and reports the + // failure itself, so the bulk actions just drive it item by item. const handleCheckAll = async () => { - try { - for (const item of Array.from(items)) { - if (!item.checked) await togglePackingItem(tripId, item.id, true) - } - } catch { toast.error(t('packing.toast.saveError')) } + for (const item of Array.from(items)) { + if (!item.checked) await togglePackingItem(tripId, item.id, true) + } } const handleUncheckAll = async () => { - try { - for (const item of Array.from(items)) { - if (item.checked) await togglePackingItem(tripId, item.id, false) - } - } catch { toast.error(t('packing.toast.saveError')) } + for (const item of Array.from(items)) { + if (item.checked) await togglePackingItem(tripId, item.id, false) + } } const handleDeleteAll = async () => { await onDeleteAll(items) @@ -304,7 +302,8 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen placeholder={t('packing.addItemPlaceholder')} style={{ flex: 1, padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)', fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontFamily: 'inherit', outline: 'none', color: 'var(--text-primary)', background: 'var(--bg-input)' }} /> - ) : (
diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index 191601a42..1b99364d9 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -497,20 +497,6 @@ function Chip({ icon, text, color = 'var(--text-secondary)', bg = 'var(--bg-hove ) } -interface RowProps { - icon: React.ReactNode - children: React.ReactNode -} - -function Row({ icon, children }: RowProps) { - return ( -
-
{icon}
-
{children}
-
- ) -} - interface ActionButtonProps { onClick: () => void variant: 'primary' | 'ghost' | 'danger' diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index d1c1eed5f..8163dfde7 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -214,19 +214,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p // eslint-disable-next-line react-hooks/exhaustive-deps }, [reservation, prefill, isOpen, selectedDayId, defaultAssignmentId, days, places, accommodations]) - // Re-hydrate hotel day range when the accommodations prop arrives after the modal opens - // (race: tripAccommodations fetch may complete after isOpen fires, leaving hotel fields empty) - useEffect(() => { - if (!isOpen || !reservation || reservation.type !== 'hotel' || !reservation.accommodation_id) return - const acc = accommodations.find(a => a.id == reservation.accommodation_id) - if (!acc) return - setForm(prev => { - if (prev.hotel_place_id !== '' || prev.hotel_start_day !== '' || prev.hotel_end_day !== '') return prev - const accPlace = places.find(p => p.id == acc.place_id) - return { ...prev, hotel_place_id: acc.place_id, hotel_start_day: acc.start_day_id, hotel_end_day: acc.end_day_id, hotel_address: accPlace?.address || prev.hotel_address } - }) - }, [accommodations, isOpen, reservation, places]) - const set = (field, value) => setForm(prev => ({ ...prev, [field]: value })) const toggleTraveler = (id: number) => setTravelerIds(prev => { @@ -807,7 +794,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p } function formatDate(dateStr, locale) { - if (!dateStr) return '' const d = new Date(dateStr + 'T00:00:00Z') return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short', timeZone: 'UTC' }) } diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index 340201735..1ce7e4f01 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -90,7 +90,6 @@ interface ReservationCardProps { } function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateToFiles, assignmentLookup, canEdit, days = [], contributions = [], detailPlugins = [] }: ReservationCardProps) { - const { toggleReservationStatus } = useTripStore() const toast = useToast() const { t, locale } = useTranslation() const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h' @@ -117,10 +116,6 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo const linked = r.assignment_id ? assignmentLookup[r.assignment_id] : null const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) - const handleToggle = async () => { - try { await toggleReservationStatus(tripId, r.id) } - catch { toast.error(t('reservations.toast.updateError')) } - } const handleDelete = async () => { setShowDeleteConfirm(false) try { await onDelete(r.id) } catch { toast.error(t('reservations.toast.deleteError')) } diff --git a/client/src/components/Planner/TransitSearchPanel.test.tsx b/client/src/components/Planner/TransitSearchPanel.test.tsx index a9c003afb..3f4bdde6c 100644 --- a/client/src/components/Planner/TransitSearchPanel.test.tsx +++ b/client/src/components/Planner/TransitSearchPanel.test.tsx @@ -456,7 +456,7 @@ describe('TransitSearchPanel', () => { }) const mixed = { ...ITINERARY, - legs: [leg('BUS', 'B1'), leg('TRAM', 'M4'), leg('FERRY', 'F10'), leg('FUNICULAR', 'FU'), leg('AERIAL_LIFT', 'AL'), leg('HIGHSPEED_RAIL', 'ICE 599')], + legs: [leg('BUS', 'B1'), leg('TRAM', 'M4'), leg('FERRY', 'F10'), leg('FUNICULAR', 'FU'), leg('AERIAL_LIFT', 'AL'), leg('SUBWAY', 'U2'), leg('HIGHSPEED_RAIL', 'ICE 599')], } transitApiMock.plan.mockResolvedValueOnce({ itineraries: [mixed] }) render() @@ -469,9 +469,11 @@ describe('TransitSearchPanel', () => { expect(icon('F10')).toContain('lucide-sailboat') expect(icon('FU')).toContain('lucide-cable-car') expect(icon('AL')).toContain('lucide-cable-car') - // Anything else (rail and friends) falls through to lucide's `Train`, which - // is an alias of TramFront — so rail and tram currently share a glyph. - expect(icon('ICE 599')).toContain('lucide-tram-front') + expect(icon('U2')).toContain('lucide-train-front-tunnel') + // Anything else (rail and friends) falls through to TrainFront, which stays + // distinct from the tram glyph. + expect(icon('ICE 599')).toContain('lucide-train-front') + expect(icon('ICE 599')).not.toContain('tunnel') }) it('FE-PLANNER-TRANSIT-026: a leg without a usable timestamp renders an empty time cell, not "Invalid Date"', async () => { diff --git a/client/src/components/Planner/TransitSearchPanel.tsx b/client/src/components/Planner/TransitSearchPanel.tsx index a1cfce9e1..f610935a1 100644 --- a/client/src/components/Planner/TransitSearchPanel.tsx +++ b/client/src/components/Planner/TransitSearchPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import tzlookup from 'tz-lookup' -import { ArrowLeftRight, ArrowRight, Bus, CableCar, ChevronDown, ChevronUp, Clock, Footprints, MapPin, Sailboat, Search, Train, TramFront, TrainFront } from 'lucide-react' +import { ArrowLeftRight, ArrowRight, Bus, CableCar, ChevronDown, ChevronUp, Clock, Footprints, MapPin, Sailboat, Search, TramFront, TrainFront, TrainFrontTunnel } from 'lucide-react' import CustomTimePicker from '../shared/CustomTimePicker' import { TransitMetaBadges } from './transitDisplay' import { transitApi } from '../../api/client' @@ -39,22 +39,24 @@ export interface PickedPlace { name: string; lat: number; lng: number } // ── helpers ────────────────────────────────────────────────────────────────── const MODE_GROUPS: { key: string; labelKey: string; Icon: React.ComponentType<{ size?: number; strokeWidth?: number }>; modes: string }[] = [ - { key: 'rail', labelKey: 'transit.mode.rail', Icon: Train, modes: 'HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN' }, - { key: 'subway', labelKey: 'transit.mode.subway', Icon: TrainFront, modes: 'SUBWAY' }, + // lucide's `Train` is an alias of TramFront, so rail keeps TrainFront and the + // subway takes the tunnel variant — otherwise the chips share a glyph. + { key: 'rail', labelKey: 'transit.mode.rail', Icon: TrainFront, modes: 'HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN' }, + { key: 'subway', labelKey: 'transit.mode.subway', Icon: TrainFrontTunnel, modes: 'SUBWAY' }, { key: 'tram', labelKey: 'transit.mode.tram', Icon: TramFront, modes: 'TRAM' }, { key: 'bus', labelKey: 'transit.mode.bus', Icon: Bus, modes: 'BUS,COACH' }, { key: 'ferry', labelKey: 'transit.mode.ferry', Icon: Sailboat, modes: 'FERRY' }, { key: 'cable', labelKey: 'transit.mode.cable', Icon: CableCar, modes: 'FUNICULAR,AERIAL_LIFT' }, ] +// Only called for non-WALK legs — walking renders its own Footprints inline. function legIcon(mode: string) { - if (mode === 'WALK') return Footprints if (mode === 'BUS' || mode === 'COACH') return Bus if (mode === 'TRAM') return TramFront - if (mode === 'SUBWAY') return TrainFront + if (mode === 'SUBWAY') return TrainFrontTunnel if (mode === 'FERRY') return Sailboat if (mode === 'FUNICULAR' || mode === 'AERIAL_LIFT') return CableCar - return Train + return TrainFront } function tzAt(lat: number, lng: number): string { diff --git a/client/src/components/Planner/usePlacesSidebar.ts b/client/src/components/Planner/usePlacesSidebar.ts index 6dbd04b41..65e904c3c 100644 --- a/client/src/components/Planner/usePlacesSidebar.ts +++ b/client/src/components/Planner/usePlacesSidebar.ts @@ -58,7 +58,6 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { const collectionsEnabled = useAddonStore((s) => s.isEnabled('collections')) // Places-API enrichment (#886) needs a Google Maps key; gate the toggle on it. const canEnrichImport = useAuthStore((s) => s.hasMapsKey) - const isNaverListImportEnabled = true const [fileImportOpen, setFileImportOpen] = useState(false) const [sidebarDropFile, setSidebarDropFile] = useState(null) @@ -106,19 +105,13 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { const [listImportLoading, setListImportLoading] = useState(false) const [listImportProvider, setListImportProvider] = useState<'google' | 'naver'>('google') const [listImportEnrich, setListImportEnrich] = useState(false) - const availableListImportProviders: Array<'google' | 'naver'> = isNaverListImportEnabled ? ['google', 'naver'] : ['google'] + const availableListImportProviders: Array<'google' | 'naver'> = ['google', 'naver'] const hasMultipleListImportProviders = availableListImportProviders.length > 1 - useEffect(() => { - if (!isNaverListImportEnabled && listImportProvider === 'naver') { - setListImportProvider('google') - } - }, [isNaverListImportEnabled, listImportProvider]) - const handleListImport = async () => { if (!listImportUrl.trim()) return setListImportLoading(true) - const provider = listImportProvider === 'naver' && isNaverListImportEnabled ? 'naver' : 'google' + const provider = listImportProvider try { const enrich = listImportEnrich && canEnrichImport const result = provider === 'google' @@ -186,7 +179,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { if (next.has(catId)) next.delete(catId); else next.add(catId) setCategoryFilters(next) } - const [dayPickerPlace, setDayPickerPlace] = useState(null) + const [dayPickerPlace, setDayPickerPlace] = useState(null) const [catDropOpen, setCatDropOpen] = useState(false) const [mobileShowDays, setMobileShowDays] = useState(false) diff --git a/client/src/components/Settings/AccountTab.tsx b/client/src/components/Settings/AccountTab.tsx index 84fc7d87d..59c601394 100644 --- a/client/src/components/Settings/AccountTab.tsx +++ b/client/src/components/Settings/AccountTab.tsx @@ -4,6 +4,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useTranslation } from '../../i18n' import { useAuthStore } from '../../store/authStore' import { useToast } from '../shared/Toast' +import { escapeHtml } from '@trek/shared' import { authApi, adminApi } from '../../api/client' import { getApiErrorMessage } from '../../types' import type { UserWithOidc } from '../../types' @@ -106,7 +107,7 @@ export default function AccountTab(): React.ReactElement { if (!backupCodesText) return const html = `TREK MFA Backup Codes -

TREK MFA Backup Codes

${new Date().toLocaleString()}

${backupCodesText}
` +

TREK MFA Backup Codes

${escapeHtml(new Date().toLocaleString())}

${escapeHtml(backupCodesText)}
` const w = window.open('', '_blank', 'width=900,height=700') if (!w) return w.document.open() diff --git a/client/src/components/Settings/MapSettingsTab.tsx b/client/src/components/Settings/MapSettingsTab.tsx index 36601e362..995014770 100644 --- a/client/src/components/Settings/MapSettingsTab.tsx +++ b/client/src/components/Settings/MapSettingsTab.tsx @@ -196,7 +196,6 @@ export default function MapSettingsTab(): React.ReactElement { setSaving(true) try { const glStyle = provider === 'leaflet' ? mapboxStyle : normalizeStyleForProvider(provider, mapboxStyle) - setMapboxStyle(glStyle) // Save into the active provider's own slot so the other provider's style survives. const stylePatch = provider === 'maplibre-gl' ? { maplibre_style: glStyle } : { mapbox_style: glStyle } await updateSettings({ @@ -207,6 +206,8 @@ export default function MapSettingsTab(): React.ReactElement { mapbox_3d_enabled: mapbox3d, mapbox_quality_mode: mapboxQuality, }) + // Only mirror the normalized style into the form once it is actually persisted. + setMapboxStyle(glStyle) toast.success(t('settings.toast.mapSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) diff --git a/client/src/components/Settings/NotificationsTab.tsx b/client/src/components/Settings/NotificationsTab.tsx index 7f6d7e80b..35f0d22f0 100644 --- a/client/src/components/Settings/NotificationsTab.tsx +++ b/client/src/components/Settings/NotificationsTab.tsx @@ -165,9 +165,9 @@ export default function NotificationsTab(): React.ReactElement { await settingsApi.setBulk({ ntfy_topic: ntfyTopic, ntfy_server: ntfyServer, - ...(ntfyToken && ntfyToken !== '••••••••' ? { ntfy_token: ntfyToken } : {}), + ...(ntfyToken ? { ntfy_token: ntfyToken } : {}), }) - if (ntfyToken && ntfyToken !== '••••••••') setNtfyTokenIsSet(true) + if (ntfyToken) setNtfyTokenIsSet(true) toast.success(t('settings.ntfyUrl.saved')) } catch { toast.error(t('common.error')) @@ -194,7 +194,9 @@ export default function NotificationsTab(): React.ReactElement { const result = await notificationsApi.testNtfy({ topic: ntfyTopic, server: ntfyServer || null, - token: ntfyToken && ntfyToken !== '••••••••' ? ntfyToken : null, + // A stored token is only masked in the placeholder, never in state — sending + // null makes the server fall back to the saved one. + token: ntfyToken || null, }) if (result.success) toast.success(t('settings.ntfyUrl.testSuccess')) else toast.error(result.error || t('settings.ntfyUrl.testFailed')) diff --git a/client/src/components/Settings/PasskeysSection.tsx b/client/src/components/Settings/PasskeysSection.tsx index a221ab6dd..a6db0329b 100644 --- a/client/src/components/Settings/PasskeysSection.tsx +++ b/client/src/components/Settings/PasskeysSection.tsx @@ -63,8 +63,9 @@ export default function PasskeysSection({ demoMode }: { demoMode?: boolean }): R const canAdd = enabled && configured + // Both step-up flows are gated by disabled={busy || !pwd} on their submit button, + // so the password is always present by the time these run. const handleAdd = async () => { - if (!addPwd) { toast.error(t('settings.passkey.passwordRequired')); return } setBusy(true) try { const options = await authApi.passkey.registerOptions(addPwd) @@ -94,7 +95,6 @@ export default function PasskeysSection({ demoMode }: { demoMode?: boolean }): R } const handleDelete = async (id: number) => { - if (!deletePwd) { toast.error(t('settings.passkey.passwordRequired')); return } setBusy(true) try { await authApi.passkey.delete(id, deletePwd) diff --git a/client/src/components/Settings/PhotoProvidersSection.tsx b/client/src/components/Settings/PhotoProvidersSection.tsx index bd1659a7a..c36fce449 100644 --- a/client/src/components/Settings/PhotoProvidersSection.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.tsx @@ -192,8 +192,11 @@ export default function PhotoProvidersSection(): React.ReactElement { if (!testPath) return setProviderTesting(prev => ({ ...prev, [provider.id]: true })) try { - const payload = buildProviderPayload(provider) - const res = cfg.test_post ? await apiClient.post(testPath, payload) : await apiClient.get(testPath) + // Only a POST probe carries the form values. A provider that declares just a + // GET (test_get / status_get) is probed against its saved credentials. + const res = cfg.test_post + ? await apiClient.post(testPath, buildProviderPayload(provider)) + : await apiClient.get(testPath) const ok = !!res.data?.connected setProviderConnected(prev => ({ ...prev, [provider.id]: ok })) if (ok) { diff --git a/client/src/components/SystemNotices/SystemNoticeBanner.tsx b/client/src/components/SystemNotices/SystemNoticeBanner.tsx index 36c3a8de4..5bfb41774 100644 --- a/client/src/components/SystemNotices/SystemNoticeBanner.tsx +++ b/client/src/components/SystemNotices/SystemNoticeBanner.tsx @@ -1,10 +1,9 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Info, AlertTriangle, AlertOctagon, X } from 'lucide-react'; import { useSystemNoticeStore } from '../../store/systemNoticeStore.js'; import type { SystemNoticeDTO } from '../../store/systemNoticeStore.js'; import { useTranslation } from '../../i18n/index.js'; -import { isRtlLanguage } from '../../i18n/index.js'; import { runNoticeAction } from './noticeActions.js'; const SEVERITY_ICONS: Record = { @@ -43,39 +42,37 @@ const SEVERITY = { interface BannerItemProps { notice: SystemNoticeDTO; onDismiss: () => void; - language: string; } function CTALink({ notice, + cta, label, onDismiss, }: { notice: SystemNoticeDTO; + cta: NonNullable; label: string; onDismiss: () => void; }) { const navigate = useNavigate(); function handleClick() { - if (!notice.cta) return; - if (notice.cta.kind === 'nav') { - navigate(notice.cta.href); + if (cta.kind === 'nav') { + navigate(cta.href); if (notice.dismissible) onDismiss(); - } else if (notice.cta.kind === 'link') { - window.open(notice.cta.href, '_blank', 'noopener,noreferrer'); + } else if (cta.kind === 'link') { + window.open(cta.href, '_blank', 'noopener,noreferrer'); } else { - runNoticeAction(notice.cta.actionId, { navigate }); - if (notice.cta.dismissOnAction !== false) onDismiss(); + runNoticeAction(cta.actionId, { navigate }); + if (cta.dismissOnAction !== false) onDismiss(); } } - if (!notice.cta) return null; - - if (notice.cta.kind === 'nav' || notice.cta.kind === 'link') { + if (cta.kind === 'nav' || cta.kind === 'link') { return ( { e.preventDefault(); handleClick(); }} className="underline hover:no-underline font-medium ml-3 shrink-0" > @@ -94,7 +91,7 @@ function CTALink({ ); } -function BannerItem({ notice, onDismiss, language }: BannerItemProps) { +function BannerItem({ notice, onDismiss }: BannerItemProps) { const { t } = useTranslation(); const s = SEVERITY[notice.severity] ?? SEVERITY.info; const title = t(notice.titleKey); @@ -121,7 +118,7 @@ function BannerItem({ notice, onDismiss, language }: BannerItemProps) { {body} )} {ctaLabel && notice.cta && ( - + )}
{notice.dismissible && ( @@ -140,10 +137,9 @@ function BannerItem({ notice, onDismiss, language }: BannerItemProps) { interface AnimatedBannerItemProps { notice: SystemNoticeDTO; onDismiss: () => void; - language: string; } -function AnimatedBannerItem({ notice, onDismiss, language }: AnimatedBannerItemProps) { +function AnimatedBannerItem({ notice, onDismiss }: AnimatedBannerItemProps) { const [mounted, setMounted] = useState(false); const prefersReducedMotion = typeof window !== 'undefined' && @@ -164,7 +160,7 @@ function AnimatedBannerItem({ notice, onDismiss, language }: AnimatedBannerItemP return (
- +
); } @@ -175,7 +171,6 @@ interface BannerRendererProps { export function BannerRenderer({ notices }: BannerRendererProps) { const { dismiss } = useSystemNoticeStore(); - const { language } = useTranslation(); const containerRef = useRef(null); // Show at most 2 highest-priority banners @@ -209,7 +204,6 @@ export function BannerRenderer({ notices }: BannerRendererProps) { dismiss(notice.id)} - language={language} /> ))} diff --git a/client/src/components/SystemNotices/SystemNoticeModal.test.tsx b/client/src/components/SystemNotices/SystemNoticeModal.test.tsx index e301473a8..74ac18938 100644 --- a/client/src/components/SystemNotices/SystemNoticeModal.test.tsx +++ b/client/src/components/SystemNotices/SystemNoticeModal.test.tsx @@ -1326,7 +1326,7 @@ describe('ModalRenderer', () => { expect(dismissSpy).not.toHaveBeenCalled(); }); - it('FE-SN-MODAL-056: pager buttons of the side slots cannot page past the ends', async () => { + it('FE-SN-MODAL-056: the side slots are inert previews, and their pagers cannot page past the ends', async () => { stubMatchMedia({ mobile: true }); const notices = [ makeNotice({ id: 'n1', titleKey: 'Notice A' }), @@ -1336,6 +1336,14 @@ describe('ModalRenderer', () => { render(); await flushGraceDelay(); + // the side slots carry a second copy of the pager and title, so they are + // kept out of the a11y tree and the tab order + const { prevSlot, nextSlot } = sheetParts(); + expect(prevSlot.hasAttribute('inert')).toBe(true); + expect(prevSlot.getAttribute('aria-hidden')).toBe('true'); + expect(nextSlot.hasAttribute('inert')).toBe(true); + expect(nextSlot.getAttribute('aria-hidden')).toBe('true'); + // the next slot renders page 2's pager, whose "previous" button is enabled — // pressing it must not move the sheet below the first page const prevButtons = screen.getAllByLabelText('Previous notice') as HTMLButtonElement[]; diff --git a/client/src/components/SystemNotices/SystemNoticeModal.tsx b/client/src/components/SystemNotices/SystemNoticeModal.tsx index a6c797eb4..293f247f2 100644 --- a/client/src/components/SystemNotices/SystemNoticeModal.tsx +++ b/client/src/components/SystemNotices/SystemNoticeModal.tsx @@ -77,7 +77,6 @@ interface ContentProps { titleId: string; bodyId: string; isDark: boolean; - onDismiss: () => void; onDismissAll: () => void; onCTA: () => void; onSecondaryCTA: () => void; @@ -90,7 +89,7 @@ interface ContentProps { onGoto: (i: number) => void; } -function NoticeContent({ notice, title, body, ctaLabel, secondaryCtaLabel, titleId, bodyId, isDark, onDismiss, onDismissAll, onCTA, onSecondaryCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) { +function NoticeContent({ notice, title, body, ctaLabel, secondaryCtaLabel, titleId, bodyId, isDark, onDismissAll, onCTA, onSecondaryCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) { const { t } = useTranslation(); const isLastPage = total <= 1 || currentPage === total - 1; @@ -383,9 +382,6 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { // Sheet scroll offset at the moment the touch began — used to suppress dismiss-drag // when the user is scrolled into content and pans down to scroll back up. const scrollTopAtTouchStart = useRef(0); - // Keep a ref to the current notice id so dismiss/CTA handlers see the latest value - const noticeIdRef = useRef(null); - noticeIdRef.current = notice?.id ?? null; // Page-slide animation refs. // isPageNavRef: set to true just before a user-initiated page change so the @@ -532,27 +528,13 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { ); } - // Dismiss current notice. The store removes it from the array, and the next - // notice naturally shifts into notices[idx]. The clamp effect handles the - // edge case where idx was pointing at the last item. - function handleDismissById(id: string) { - setVisible(false); - dismiss(id); - } - - function handleDismiss() { - const id = noticeIdRef.current; - if (id) handleDismissById(id); - } - // Dismiss every notice in the current modal list — used by the X button and ESC. function handleDismissAll() { setVisible(false); notices.forEach(n => dismiss(n.id)); } - function runCta(cta: SystemNoticeDTO['cta']) { - if (!cta) { handleDismissAll(); return; } + function runCta(cta: NonNullable) { if (cta.kind === 'nav') { navigate(cta.href); if (notice?.dismissible !== false) handleDismissAll(); @@ -565,8 +547,9 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { if (cta.dismissOnAction !== false) handleDismissAll(); } } - function handleCTA() { runCta(notice?.cta); } - function handleSecondaryCTA() { runCta(notice?.secondaryCta); } + // Both buttons only render when the notice carries the matching cta. + function handleCTA() { runCta(notice!.cta!); } + function handleSecondaryCTA() { runCta(notice!.secondaryCta!); } function animatedDismissAll() { const sheet = sheetRef.current; @@ -626,7 +609,7 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { notice, canPage, isLastPage, language, t, dur, ease, touchStartX, touchStartY, dragLockRef, scrollTopAtTouchStart, isPageNavRef, stripRef, sheetRef, prevSlotRef, contentWrapperRef, nextSlotRef, - announceIndex, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, animatedDismissAll, + announceIndex, handleDismissAll, handleCTA, handleSecondaryCTA, animatedDismissAll, handlePrev, handleNext, handleGoto, }; } @@ -635,7 +618,7 @@ type NoticeState = ReturnType; // Build the NoticeContent props for a given notice + pager slot index. function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number): ContentProps { - const { t, isDark, canPage, notices, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, handlePrev, handleNext, handleGoto } = S; + const { t, isDark, canPage, notices, handleDismissAll, handleCTA, handleSecondaryCTA, handlePrev, handleNext, handleGoto } = S; const rawBody = t(n.bodyKey); const body = n.bodyParams ? Object.entries(n.bodyParams).reduce( @@ -652,7 +635,6 @@ function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number): titleId: `notice-title-${n.id}`, bodyId: `notice-body-${n.id}`, isDark, - onDismiss: handleDismiss, onDismissAll: handleDismissAll, onCTA: handleCTA, onSecondaryCTA: handleSecondaryCTA, @@ -823,13 +805,16 @@ function MobileNoticeSheet(S: NoticeState) { ref={stripRef} style={{ display: 'flex', width: '300%', height: '100%', alignItems: 'stretch', transform: 'translateX(-33.333%)' }} > -
+ {/* The side slots are drag previews only: their pager and CTA are a + second copy of the centre ones, so they stay out of the a11y tree + and the tab order until they become the centre slot. */} +
-
+
diff --git a/client/src/components/Todo/TodoListPanel.tsx b/client/src/components/Todo/TodoListPanel.tsx index 482a14e9a..146526e77 100644 --- a/client/src/components/Todo/TodoListPanel.tsx +++ b/client/src/components/Todo/TodoListPanel.tsx @@ -22,6 +22,53 @@ import TodoRow from './TodoRow' import { usePluginViewContributions, PluginCardFooter } from '../Plugins/PluginContributions' import EmptyState from '../shared/EmptyState' +// Sidebar filter row. Declared at module level so React keeps the same component +// type across renders — inline it and every re-render remounts the buttons, +// which drops clicks that are already in flight. +function SidebarItem({ id, icon: Icon, label, count, color, active, compact, onSelect }: { + id: string + icon: any + label: string + count: number + color?: string + active: boolean + compact: boolean + onSelect: (id: string) => void +}) { + return ( + + ) +} + export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tripId: number; items: TodoItem[]; addItemSignal?: number }) { // Layout component: state/effects/derived/handlers live in useTodoList. const { @@ -62,39 +109,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr reorderTodoItems(tripId, globalIds) } - // Sidebar filter item - const SidebarItem = ({ id, icon: Icon, label, count, color }: { id: string; icon: any; label: string; count: number; color?: string }) => ( - - ) + const selectFilter = (id: string) => setFilter(id as FilterType) // Filter title const filterTitle = (() => { @@ -138,10 +153,10 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr {!isMobile &&
{t('todo.sidebar.tasks')}
} - !i.checked).length} /> - - - + !i.checked).length} active={filter === 'all'} compact={isMobile} onSelect={selectFilter} /> + + + {/* Sort by */} {!isMobile &&
@@ -169,7 +184,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
} {isMobile &&
} {categories.map(cat => ( - + ))} {canEdit && ( diff --git a/client/src/components/Todo/useTodoList.ts b/client/src/components/Todo/useTodoList.ts index 1ccd1f7c0..afda45b4b 100644 --- a/client/src/components/Todo/useTodoList.ts +++ b/client/src/components/Todo/useTodoList.ts @@ -71,7 +71,8 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu let result: TodoItem[] if (filter === 'all') result = items.filter(i => !i.checked) else if (filter === 'done') result = items.filter(i => !!i.checked) - else if (filter === 'my') result = items.filter(i => !i.checked && i.assigned_user_id === currentUserId) + // No resolved user means nothing is "mine" — matching the myCount badge. + else if (filter === 'my') result = currentUserId ? items.filter(i => !i.checked && i.assigned_user_id === currentUserId) : [] else if (filter === 'overdue') result = items.filter(i => !i.checked && i.due_date && i.due_date < today) else result = items.filter(i => i.category === filter) if (sortByPrio) result = [...result].sort((a, b) => { diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index 641df712b..fa28e3447 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -37,7 +37,7 @@ interface CoverSearchPhoto { export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUpdate }: TripFormModalProps) { const isEditing = !!trip - const fileRef = useRef(null) + const fileRef = useRef(null) const coverSearchSeq = useRef(0) const toast = useToast() const { t } = useTranslation() @@ -202,7 +202,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } - const handleCoverSelect = async (file) => { + const handleCoverSelect = async (file: File | null | undefined) => { if (!file) return // HEIC/HEIF from iOS can't be rendered or stored as-is — convert to JPEG first const normalized = await normalizeImageFile(file) @@ -217,12 +217,12 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } - const handleCoverChange = (e) => { - handleCoverSelect((e.target as HTMLInputElement).files?.[0]) + const handleCoverChange = (e: React.ChangeEvent) => { + handleCoverSelect(e.target.files?.[0]) e.target.value = '' } - const uploadCoverNow = async (file) => { + const uploadCoverNow = async (file: File) => { setUploadingCover(true) try { const fd = new FormData() @@ -291,7 +291,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp setCoverPreview(null) return } - if (!trip?.id) return + // Nothing pending left, so the preview is a saved trip's stored cover. try { await tripsApi.update(trip.id, { cover_image: null }) setCoverPreview(null) @@ -613,8 +613,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp {selectedMembers.length > 0 && (
{selectedMembers.map(uid => { - const user = allUsers.find(u => u.id === uid) - if (!user) return null + const user = allUsers.find(u => u.id === uid)! return ( setSelectedMembers(prev => prev.filter(id => id !== uid))} className="bg-surface-secondary text-content border border-edge cursor-pointer" @@ -633,7 +632,6 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp { - if (!value) return if (isEditing && trip?.id) { const user = allUsers.find(u => u.id === Number(value)) if (user) { diff --git a/client/src/components/Trips/TripMembersModal.test.tsx b/client/src/components/Trips/TripMembersModal.test.tsx index 0e8042e8d..47db5a5c9 100644 --- a/client/src/components/Trips/TripMembersModal.test.tsx +++ b/client/src/components/Trips/TripMembersModal.test.tsx @@ -869,6 +869,23 @@ describe('TripMembersModal', () => { await waitFor(() => expect(screen.queryByDisplayValue('Granny')).not.toBeInTheDocument()); }); + it('FE-COMP-MEMBERS-048b: Enter followed by a blur renames only once', async () => { + const user = userEvent.setup(); + mockRoster([guestRow]); + let renames = 0; + server.use(http.put('/api/trips/1/guests/:userId', () => { renames++; return HttpResponse.json({ success: true }); })); + render(); + + await user.click(await screen.findByTitle('Rename')); + const input = screen.getByDisplayValue('Grandma'); + fireEvent.change(input, { target: { value: 'Granny' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + fireEvent.blur(input); + + await waitFor(() => expect(screen.queryByDisplayValue('Granny')).not.toBeInTheDocument()); + expect(renames).toBe(1); + }); + it('FE-COMP-MEMBERS-049: Escape cancels a rename and a blank name saves nothing', async () => { const user = userEvent.setup(); mockRoster([guestRow]); diff --git a/client/src/components/Trips/TripMembersModal.tsx b/client/src/components/Trips/TripMembersModal.tsx index d9c32d8bd..b834e2127 100644 --- a/client/src/components/Trips/TripMembersModal.tsx +++ b/client/src/components/Trips/TripMembersModal.tsx @@ -202,7 +202,6 @@ function TripInviteLinkSection({ tripId, t }: { tripId: number; t: (key: string, } const copy = () => { - if (!inviteUrl) return navigator.clipboard.writeText(inviteUrl) setCopied(true) if (copyTimerRef.current) clearTimeout(copyTimerRef.current) @@ -324,7 +323,6 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o } const handleAdd = async () => { - if (!selectedUserId) return setAdding(true) try { const target = allUsers.find(u => String(u.id) === String(selectedUserId)) @@ -370,7 +368,14 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o } } + // Enter commits, and the blur that follows would send the same rename a second + // time. Only the first commit per editing session goes through; a failed one + // reopens the gate so the user can retry from the still-open input. + const renameCommittedRef = useRef(false) + const handleRenameGuest = async (userId) => { + if (renameCommittedRef.current) return + renameCommittedRef.current = true const name = renameValue.trim() if (!name) { setRenamingGuestId(null); return } try { @@ -378,6 +383,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o setRenamingGuestId(null) await loadMembers(true) } catch (err: unknown) { + renameCommittedRef.current = false toast.error(getApiErrorMessage(err, t('members.guestRenameError'))) } } @@ -572,7 +578,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o autoFocus value={renameValue} onChange={e => setRenameValue(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleRenameGuest(g.id); if (e.key === 'Escape') setRenamingGuestId(null) }} + onKeyDown={e => { if (e.key === 'Enter') handleRenameGuest(g.id); if (e.key === 'Escape') { renameCommittedRef.current = true; setRenamingGuestId(null) } }} onBlur={() => handleRenameGuest(g.id)} maxLength={50} className="bg-surface border border-edge text-content" @@ -589,7 +595,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o {isCurrentOwner && renamingGuestId !== g.id && ( <> + ) +} + // Dev-only notification testing panel, re-skinned to the mobile admin system. // All state, fetches and fire() payloads are ported verbatim from the desktop // DevNotificationsPanel — only the presentation layer changes. @@ -50,8 +86,8 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { try { await adminApi.sendTestNotification(payload) toast.success(`Sent: ${label}`) - } catch (err: any) { - toast.error(err.message || 'Failed') + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : 'Failed') } finally { setSending(null) } @@ -62,53 +98,6 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { const username = user?.username || 'Admin' const tripTitle = selectedTrip?.title || 'Test Trip' - // ── Helpers ────────────────────────────────────────────────────────────── - - const Btn = ({ - id, label, sub, icon: Icon, color, onClick, - }: { - id: string; label: string; sub: string; icon: LucideIcon; color: string; onClick: () => void - }) => ( - - ) - - const TripSelector = () => ( - - ) - - const UserSelector = () => ( - - ) - return (
{/* Dev header */} @@ -128,7 +117,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Test how each in-app notification type renders, sent to yourself." />
- fire('simple-me', { event: 'test_simple', scope: 'user', @@ -136,7 +125,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: {}, })} /> - fire('boolean-me', { event: 'test_boolean', scope: 'user', @@ -149,7 +138,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { }, })} /> - fire('navigate-me', { event: 'test_navigate', scope: 'user', @@ -157,7 +146,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: {}, })} /> - fire('simple-admins', { event: 'test_simple', scope: 'admin', @@ -176,9 +165,15 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Fires each trip event to all members of the selected trip (excluding yourself)." />
- +
- selectedTripId && fire('booking_change', { event: 'booking_change', scope: 'trip', @@ -186,7 +181,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, booking: 'Test Hotel', type: 'hotel', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('trip_reminder', { event: 'trip_reminder', scope: 'trip', @@ -194,7 +189,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { trip: tripTitle, tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('photos_shared', { event: 'photos_shared', scope: 'trip', @@ -202,7 +197,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('collab_message', { event: 'collab_message', scope: 'trip', @@ -210,7 +205,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, preview: 'This is a test message preview.', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('packing_tagged', { event: 'packing_tagged', scope: 'trip', @@ -231,9 +226,16 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Fires each user event to the selected recipient." />
- +
- fire('version_available', { event: 'version_available', scope: 'admin', diff --git a/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx b/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx index 1b03a6ee4..034c0565d 100644 --- a/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx @@ -6,6 +6,7 @@ import { MAdminButton, MAdminCard } from './MAdminUi' const REPO = 'mauriceboe/TREK' const PER_PAGE = 10 +const MAX_PAGES_PER_LOAD = 5 interface GithubRelease { id: number @@ -39,27 +40,49 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea const [hasMore, setHasMore] = useState(true) const [loadingMore, setLoadingMore] = useState(false) - const fetchReleases = async (pageNum = 1, append = false) => { + const isShown = (release: GithubRelease) => isPrerelease || !release.prerelease + + const fetchPage = async (pageNum: number) => { try { const res = await apiClient.get(`/admin/github-releases`, { params: { per_page: PER_PAGE, page: pageNum } }) - const data = Array.isArray(res.data) ? res.data : [] - setReleases(prev => append ? [...prev, ...data] : data) - setHasMore(data.length === PER_PAGE) + return Array.isArray(res.data) ? res.data as GithubRelease[] : [] } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Unknown error') + return null } } + // Keep pulling pages until at least one release survives the prerelease filter, + // otherwise a page of nothing but prereleases leaves an empty timeline behind a + // "Load more" button. MAX_PAGES_PER_LOAD bounds the walk. + const loadFrom = async (startPage: number, append: boolean) => { + const collected: GithubRelease[] = [] + let pageNum = startPage + let more = true + + for (let i = 0; i < MAX_PAGES_PER_LOAD; i++) { + const data = await fetchPage(pageNum) + if (!data) return + collected.push(...data) + more = data.length === PER_PAGE + if (!more || collected.some(isShown)) break + pageNum += 1 + } + + setReleases(prev => append ? [...prev, ...collected] : collected) + setHasMore(more) + setPage(pageNum) + setError(null) + } + useEffect(() => { setLoading(true) - fetchReleases(1).finally(() => setLoading(false)) + loadFrom(1, false).finally(() => setLoading(false)) }, []) const handleLoadMore = async () => { - const next = page + 1 setLoadingMore(true) - await fetchReleases(next, true) - setPage(next) + await loadFrom(page + 1, true) setLoadingMore(false) } @@ -152,7 +175,7 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea { href: 'https://github.com/mauriceboe/TREK/wiki', color: '#6366f1', icon: , title: 'Wiki', sub: t('settings.about.wikiHint') }, ] - const shownReleases = isPrerelease ? releases : releases.filter(r => !r.prerelease) + const shownReleases = releases.filter(isShown) return (
@@ -188,7 +211,7 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea
- ) : error ? ( + ) : error && releases.length === 0 ? (

{t('admin.github.error')}

@@ -301,6 +324,13 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea
+ {/* A failed "Load more" keeps the timeline and stays retryable */} + {error && ( +

+ {t('admin.github.error')} — {error} +

+ )} + {/* Load more */} {hasMore && (
diff --git a/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx b/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx index 0c35dca5b..f08873d5f 100644 --- a/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { Key, Trash2, User, Loader2, Shield } from 'lucide-react' import { adminApi } from '../../../api/client' import { useToast } from '../../../components/shared/Toast' @@ -52,15 +52,20 @@ export default function MAdminMcpTokensPanel() { const toast = useToast() const { t, locale } = useTranslation() + // The loader runs once, but a language switch while the requests are in flight + // must still toast in the current locale — hence the ref instead of the closure. + const latest = useRef({ t, toast }) + latest.current = { t, toast } + useEffect(() => { adminApi.oauthSessions() .then(d => setSessions(d.sessions || [])) - .catch(() => toast.error(t('admin.oauthSessions.loadError'))) + .catch(() => latest.current.toast.error(latest.current.t('admin.oauthSessions.loadError'))) .finally(() => setSessionsLoading(false)) adminApi.mcpTokens() .then(d => setTokens(d.tokens || [])) - .catch(() => toast.error(t('admin.mcpTokens.loadError'))) + .catch(() => latest.current.toast.error(latest.current.t('admin.mcpTokens.loadError'))) .finally(() => setTokensLoading(false)) }, []) diff --git a/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx b/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx index 6abc10eba..9655bde49 100644 --- a/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx +++ b/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx @@ -119,7 +119,6 @@ export default function MAdminNotificationsSection({ admin, t }: MAdminNotificat const testAdminWebhook = async () => { const url = smtpValues.admin_webhook_url === '••••••••' ? undefined : smtpValues.admin_webhook_url - if (!url && smtpValues.admin_webhook_url !== '••••••••') return try { if (url) await authApi.updateAppSettings({ admin_webhook_url: url }).catch(() => {}) const result = await notificationsApi.testWebhook(url) diff --git a/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx b/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx index 88cd93df5..091556361 100644 --- a/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx +++ b/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx @@ -77,16 +77,18 @@ export default function MAdminNotifyMatrix({ t, toast }: { t: TranslationFn; toa key={ch} className="w-[44px] flex-none text-center font-geist text-[0.5625rem] font-bold uppercase tracking-[0.04em] text-m-faint" > - {t(ADMIN_CHANNEL_LABEL_KEYS[ch]) || ch} + {t(ADMIN_CHANNEL_LABEL_KEYS[ch])} ))}
{matrix.event_types.map((eventType) => { const implemented = matrix.implemented_combos[eventType] ?? [] + // Only version_available has a label key so far; anything else keeps its raw id. + const labelKey = ADMIN_EVENT_LABEL_KEYS[eventType] return (
- {t(ADMIN_EVENT_LABEL_KEYS[eventType]) || eventType} + {labelKey ? t(labelKey) : eventType} {visibleChannels.map((ch) => ( diff --git a/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx b/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx index e33c59374..6b352827c 100644 --- a/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx +++ b/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx @@ -162,7 +162,7 @@ export default function MAdminPackingTemplateManager() { await adminApi.deleteTemplateCategory(expandedId, catId) setCategories(prev => prev.filter(c => c.id !== catId)) setItems(prev => prev.filter(i => i.category_id !== catId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.toast.deleteError')) } } // Item CRUD @@ -190,7 +190,7 @@ export default function MAdminPackingTemplateManager() { try { await adminApi.deleteTemplateItem(expandedId, itemId) setItems(prev => prev.filter(i => i.id !== itemId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.toast.deleteError')) } } return ( diff --git a/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx b/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx index cac92beb1..bee362cb0 100644 --- a/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx @@ -107,6 +107,8 @@ interface RegistryDetail extends RegistryItem { icon: string | null requiredAddons?: string[] pluginDependencies?: PluginDep[] + /** Display slice of the manifest's capabilities — drives the same chips as an installed row. */ + capabilities?: { widget?: { slot?: string }; tripPage?: { replaces?: string[] } } } | null } @@ -309,13 +311,8 @@ const CHIP_PENDING = 'text-[color:var(--m-st-pending)] border-[color:color-mix(i const PENDING_CARD = 'border-[color:color-mix(in_srgb,var(--m-st-pending)_28%,transparent)] bg-[color:color-mix(in_srgb,var(--m-st-pending)_10%,transparent)]' const ACT_PILL = 'inline-flex flex-none items-center justify-center gap-[5px] whitespace-nowrap rounded-full px-3 py-[7px] text-[0.6875rem] font-bold bg-m-act text-m-actfg disabled:opacity-50' -function ReviewedBadge({ t, compact }: { t: T; compact?: boolean }) { - if (compact) return - return ( - - {t('admin.plugins.reviewed')} - - ) +function ReviewedBadge({ t }: { t: T }) { + return } /** Marks a manually-uploaded (sideloaded) plugin: no registry, unsigned, not reviewed. */ @@ -533,7 +530,12 @@ export default function MAdminPluginsPanel() { if (!runtimeOn || !Array.from(e.dataTransfer.types).includes('Files')) return e.preventDefault(); dragDepth.current++; setDragActive(true) } - const onDragLeave = () => { if (--dragDepth.current <= 0) { dragDepth.current = 0; setDragActive(false) } } + // Mirrors onDragEnter: a leave that had no matching enter (a non-file drag) is ignored + // instead of driving the depth counter negative. + const onDragLeave = () => { + if (dragDepth.current === 0) return + if (--dragDepth.current === 0) setDragActive(false) + } const onDrop = (e: DragEvent) => { e.preventDefault(); dragDepth.current = 0; setDragActive(false) if (!runtimeOn) return @@ -1146,7 +1148,7 @@ function InstalledRow({ p, t, busy, hasUpdate, latestVer, blocked, onToggle, onU
{p.name} {p.version && v{p.version}} - {p.reviewed_at && } + {p.reviewed_at && } {p.source_repo === 'local:upload' && } {p.source_repo === 'local:link' && } {/* Registry plugins only — a sideloaded/dev-linked plugin already says something @@ -1395,7 +1397,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local }, [item.id]) const manifest = detail?.manifest ?? null - const caps = manifest ? deriveCaps(manifest.permissions, {}, t) : [] + const caps = manifest ? deriveCaps(manifest.permissions, manifest.capabilities ?? {}, t) : [] const repoUrl = `https://github.com/${item.repo}` const homepage = item.homepage && /^https?:\/\//i.test(item.homepage) && item.homepage !== repoUrl ? item.homepage : null const sizeKb = detail?.size ? Math.max(1, Math.round(detail.size / 1024)) : null @@ -1421,7 +1423,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local

{item.name}

- {item.reviewedAt && } + {item.reviewedAt && }

{item.author}{item.latest ? ` · v${item.latest}` : ''}

diff --git a/client/src/mobile/screens/admin/MAdminSheets.tsx b/client/src/mobile/screens/admin/MAdminSheets.tsx index 8bb1bfbd4..e3e743d6d 100644 --- a/client/src/mobile/screens/admin/MAdminSheets.tsx +++ b/client/src/mobile/screens/admin/MAdminSheets.tsx @@ -210,7 +210,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {

{updateInfo?.is_docker === false ? (