Timeline performance improvements

This commit is contained in:
Nick Beckley 2026-06-02 21:43:31 +01:00
parent 92aff32155
commit 1dad31efab
25 changed files with 711 additions and 75 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/bOhD8KwLPpsIvUaJugdPiGXltjNZoi94o7qoholAGSaUNAv/3grKLNKeHdyGIFWc5OzPUCwn2EUlPro/Hd08YciIbkv1aEkxoyjRl4sBN38iet7XkmstWfCcb4m/xmfSkVc5xZ5BK1EgVsIYOcnQUAA1nHJhoVOlpfyTSvxDra8An77Ce0xeMyc+B9OSrDzD/LHdbX7tpXqCO84S7kLINrpD5uNve0n+v94e7/flUwlTAtYdCFUTHJHR0YK2iCnVLDQdFXWMG7Zi0QpgzzIeMMdipTvDwej/MOWB2vaiFrBkVhpw2BGy2hfJgEx6ej4XFKtFNqW/W8rrtX8gTRtKLzR8xz+2u3OSvHtOw+Amuzmu26SGt62hdnuMz2ZBjnI8Ys8dVn+uU8HGYng82Pbw/nynb9O4XuiUjbOclZNITrjjZkFWgabLZz6G6cNxOHkNeP+boQ/Luv3oVf+4+V/vJ5nGOj2/8+HyWefc/VP5g3b0PWO1uSE8YRwGyQzoKrqnqwFLbSEO1lnpE7kwryiDbOWQflnlJ1S5k/BHXkUhP3tsp4ZnfdXT3PqPLSyxzftOF9z7OsLhcvXrJWc1qyVgBLfm45OoTgo/oMpYhDnG59PvyFiK0WEHbJUYMudrbfF/9Y9M96YmxnePWWGOtdm7EhuHIgFnNWkANGkEJaEEZBVoZ5rSyA1dgGz6MpUI2BEt0qjWtbByN0cxRK1tGVdc6OnSOU9N2nInOgjAlYK86VndTwVnXMYUgqcERqDLcUqOkpCAb2wo9tq0Y/5LkkgfOisCAT+d3tzLQAB2C5lQo3lBlDKedaVo6tN1gBt1h4wrnW4wBp7+E+uBdnNM85uqSkOqNdJ+WUP4d1ZqWnz7QX6tHN7N7wFgVj60P5alcbD2dTqffAAAA//8DAH1Y7jeCBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aIveaJHuCHXkgS2nLYL57wt5pki7t70JlEg+fF/qhUV4Itax29Ppr2eKaWElS2ELKaEsF5YL9ShdV+tONpVW2igjv7CShXv6yTrWGO+ld8Q1WeIGRc17PXiOSE4KiULVJteErwvrXhiECuk5eKqm5SPNS5gi69inEHH6nntDqPw4rVjN00j7uCSIPsP8s9/d87e3h8eHw+XVQktOrgJmVFSt0NjyXjSGG7INdxIN97XrrRcalHKXtBATzRHGasFvr/1xSpGS71SldCW4cuxcMoQEGbmHhR5/njLFJtFdjpdbeDt2L+yZZtap8peYl3I3fgw3fnp6gog3Q4hhObKSnebpRHMKtMmxTTiOkMIUi2uT3RgoJlayhw/FYYQ0TPPTHxp9uIy+/x+TvwN/DJGK/R3rmJCkULfEByUtNy0Ch1o7bq22A0nvGiVZyXZTTCGu07oU+5jo67xRso79DeNCF77b2R9DIp/WOaN/ttlr+hHSbsIcEKxkh3nC1afiVW4pKlFpkS8f1nRaU/GeMMzkE+WZHuf1Wv4/KcqqLWm3zjPFVBwgHYs3sBxZxxy0XoIDB2C9H6gWNAgUYEWDZNESGoUNGmfQGie8NdBLg1DLfsiRzJ3dLbaFGtABoTC8h6blpjaSO6uBSwutVVobkBnkVdbiYcx54FthCDV3NCA3TgJ3RmuOuoZG2aFp1PCbJFfHpch6Iz1fvsZGYBFbQiu5MrLmxjnJW1c3vG/a3vW2pdpn5nuaI42/CfUu+HlapiEV14Up/pDu/Rrz9y625fkeIv+xWXY3+W80F9lyCDFv89Xl8/l8/hcAAP//AwCN39GdJQQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOGmWcRGW/Q25IxiIVppQVGbBob/e0HtFol7640YcIYvn3lnXtkMR2I9uz2dfjrTnFdWszzuISWU5cJyoZ6l71vXy7ZxznSik3+xmo2P9I31zJkYZfTENVniBkXLgx4iRyQvhUShWlNqwueV9a8MxgbpPEZqlvV3Suu4zKxnf4wzLl/L2zA2cVo2bNIy0cO8ZphjEfPbw90j//X28PzxcLm10lqSmxFZz8AHMN1gOanguAEgHlwAThGJWgKpTbykjXOmNMPUrPjl+/u45Jly7FWjdCO48uytZggZiuQAKz1/OxUVO6L7Eq/38H7sX9mZEutV/S/MS7mbOI036xYmOtN0grRSuonL8Qgzspqd0nKilEfasZwphWUdcyEao4OukxI7cKZ12OlWaYdaEHbCtejRgzV2ULFtBwydd2YwqKRBS8YEx2q2g5smyOMyV1ftd9NIc2Y1+/hUHSbIw5KO79A/XYg+/A+gRTbrmRatts6R0VqDt0EJQmejtSpEEG4IimQk0XrjvHRCaq+E1611nUMQvlT6APFlnKl6uGc9E5IU6o74oKTlpkPg0GrPrdV2IBm9U5LV7G6Z8zhvy7ZWD3Omz2n/L+vZzzCtdPnpbYovY6aYt1Qg/GmLGQ9pwS3m6rsBpGhEo4UoSVs+bbn6RDgmipkKjue0Xev9J0VZtSfdbSnRnKsD5JfqF1hfWM88dFGCBw9gYxyoFTQIFGCFQ7JoCY1Ch8YbtMaLaA0EaRBaGYYSYTWj4rdqtziBH2hAy8Fp4qZTinchOC6UNcHpIAWGdxyrj9PeydgJQ6i5pwG58RK4N1pz1C04ZQfn1PADkqtZpCiAkc6XYd0VWMSO0EqujGy58V7yzreOB9cFH2xHbSyaHynNNP0A6sMY07IuQ66uXqveofu0zWXhVLvvvo4z/3vv0f0Sv1CqSo9hnMt8Xdv6VrMjwbolOu77qn9lhzJc1fO+trRopNK6Zk8ZUt5O17DVjZGqfXt7e/sHAAD//wMAlFq6YfQEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOhtSI5sIVpqQVGbBsb+94DaDRL31psw0DszfOadeSMRDkh6cns8/nLCmBdSkzxuIcGEpkxTJvbc9q3pedsYqXkn+T+kJuMjfiU9Mcp77i1SiRqpCqylTg6ehoCWMx6YaFXJCS8L6d8IjE3A0+ixmZdPmJZxjqQnf40xzF9KbRgbP81raNI84UNcMkRfmvnz4e6R/nG72z/tLn8tuBRxMwbSE7AOVDdoisIZqgCQOuOAog+ILQKXyl9kY8yYIkzNEj7/qB/mHDH7XjRCNowKS841CZChtOxgwf3XY+liQ3Rf4vUW3j77N3LCRHpRf4d5SXfjp/Emz8cJTzgdIS2Ybvx8OEAMpCbHNB8x5RE3LNtLpwnyOMfqWuxuGjFmUpOn52o3QR7mdHjH6vmC4OF/EDhhcqQnkrVSG4NKSglWO8EwGO21Fs4DM4MTyD2y1ipjuWFcWsGsbLXpTABmS6aXaXYw/b5s/KxgrgvMCC1QoRVKa+sZyg6H1iALoLXTnbdBdkaoLnQSLAC3Do232heDfAD/OkasHu5JTxhHEWSHdBBcU9UFoNBKS7WWekDurRGc1ORujnmM67wu1UPM+JI2fqQnv8K04IXcbfKvY0af11Sg/q1LsV2aw+pz9cMBnDWskYwV0ZqPa64+YhgT+owF7z6t13z/kQgtNtHdmhLGXO0gv1a/wfJaqEDnOdjyVO39gC3DgQUGmpmAOmgMSgQTlFVBK8u8VuC4CtByN5QIqQkWw1WbxwdoUXZgqJC+parVnlqwnHZdkIDGO8b9O47V07Q5w3dMYZDU4hCoshyoVVLSIFswQg/GiOEnJFfzcVYABzxdtnXrQIfQYdCcCsVbqmwpbltDnemcdbrD1peeHzFFnH4C9WH0aV7mIVdX71bv0H1cY7k41ebjL2Ok/24zup/9Z0xVmTGMsSzYdaznmhwQljXhYTtY/RvZle2q9tvdkqzhQsqaPGdIeT1ew1o2iov2fD6fvwEAAP//AwBUXko/9QQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUzW7jNhSFX0Xg2lRIiqJE7VKngxqZTIxJOi26uyKvHCIyaVBUpkGQdx9QdhBnBl10J1zx/n3nkC/Ewx5JRy4Ph9+f0KeJrEhyS0gwoShTlIl7rru66XhdtqzmrFL/kBVx1/hMOtJIY7jRSCtUSKVlNe2rwVBrUXPGLRO1zDVhN5HuhYArLT45g2WYvmGcXPCkI385b8P33BtcacYw2zKGETd+SuBNHubLZn1NP19u72+3x1MTTjm5dJZ0BHQPsh0URdE3VAIg7ZseKBqLWCPwSppjmvMJo4exnOzje38bksdkOlGKqmRUaPK6IhYS5JF7mPD++ZCnWBBd5fhqCS+f3Qt5wkg6sXqDeSx3YUZ3sZ/62Y32Ygy7nfO7dfCD280RUm68IocYDhiTwwXOb85DfP4cdrtcj3yCcUKyIvcY987DePxxhQPMY7oLc1zITPbxv85kMnMKZEUWkuO4tC1Oy6xHhz79knvW+Pau2I6QhhD3H0S6O7Lf/A/0N2AenMdic0U6wjgKW7VIB8EVla0FCnWlqVKVGpAb3QhOVmQdfHJ+DvNUbHzC3Ynah/kuo3lwCU2aY97pb5XNto3BziYV7wJzVrKyYiwnzekwp+IrWhfRJMxL3Mf5VO+nFKHEkvTJjfgLnvUcI/pUbCE9FH/A9EA6oqE1HDRoAGXMgDXDgVkGijUWlVVopbCNlVpaJTUzSkLPpYWa90OOkBXBbLJi8TUfBGhAQ3mLNZVVjxS0MLSqGylkpfUwDB/gFrfjIoppmURbUY2DpVJzoFpWFbVVDY1QQ9OI4YzTyRCcZernJvxzQnvy0pejtd92t/h0vMfLnMraFq3iVEheU6k1p62uG9o3ba971WJt8mbr4KfwBvIbxj5MLuVH5MZ5t4fx5yPvylxj9DieqXPjTAxTGFJxsmXxQa+vs8+vWLFY9Lvz9N/FGFfBPGIssrHA+TMxX19fX38AAAD//wMAr++TXgwFAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJS1AcF7MFwWqyRpjESd3exN4ocxkRk0aAop0GR/76gJNdO0cPehNF8vHnvDX+gXh4ANWh1PH46QR8GlKJgpxAjrMSkxITtqGiKqqFlxipOKaP/ohTZO3hDDaq4UlQJwDmUgLkmBW5zo7DWICihmrCCx57yeUDNDyRtpuFkFWRu+Av8YF2PGvS37bV7jbOlzVTnRp1518GmH4LsVQTzdbO+w19W293Dds4aYIjFmdWoQVK0ktemxMDaCnMpAbdVKzEoDVCApDlXc5ntA/hedtmgXy7ztQs9BNWwjOUZwUyg9xRpGWSE3MoBdm/HiGKi6DbG0yk8fTY/0Ak8alh6JnNud6M6e3MY2tF2+iZI/wzBeHmAV+df4CQ7lKKjd0fwwcLEzcNTcoFESUYyVjJCUIrupdrbHpLNbfLQTQurmnDQORZgNOaCSix4nmOdF7JipakqZlCK7sD30F11vbfKu8GZkCyUJx/mrF0fbD+6cUg2fYBnL8Nc91l2A6AUbce2s8P+EaR+27nHMf7rxy7ush69hz4kWxn2yZ9y2KMGCVkrKoUUUpZKGSgIGKKJLEmlodQlaM50pbnguuSCqJLLlnItC9qaGEEpehjDcQxn/r9fgdh5eziAviDYTRxvOxmM84eNhj5YY6M054zJUF03bZUsPdedhT58IBk1iFBgOq8BG0ZLzGstsSxygcsyLw1QJSpGZ8KMfR5/8kSM0Tw3RV2XrWgpAWaqmpPKtEQrkyuhixYqVpGqhlooQtqC07oVimigBddxO+/0qMKvVsgngR7HPl5nsolrv9oefy/5hZGtd8Ep10XBbAdPb0OAw+9UW7vD0Q02xP3NIu23Ae5dH/8u979wthSvXLjEIN5BMp2eriSYghdYVBIwNzVgCdzgGog2NSeEqAKl6NapF/BJNJi0/aTJ2VMPT8nKq70NoMLo4+R/pqVmOT+fT+bCR/b1027tPKyOx3SJ/nGKJM3Nzgb48K58G2L63g1xi+DHKx89QWfOuK7cpOE0v1Q2Bkuta9AlxYzTAnMhKK5FUeG2qlvRljUU6uLW5BG09aDC1G83T1uI/Z0tVz5YI1UY4u18cWpy02LPJeVXkFeq/ayeh18ptzhp6URJdOzT/G5OBvpfz+b7+/v7fwAAAP//AwB0fDGlJwYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadHbiBzFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CgGfCXq4PZ3evVBIC5SQ/FaSXGrGNePyKEzftL2sq7btlNT1v1CCv6fv0EOrrBXWEKtJE1OON2yoR8ucIyO4cFw2KvfELwv0r4C+cvTiLVXz8pni4ucAPfztg5u/5tnoKzvNq6viPNE+LAmDzWQ+7Hf37K/bw/HhcHm10JLBlXfQA5oBVTdqRnJomUIkNrQDMrKOqCEUtbIXmA+JYsCpWtzT23w3p0DJ9rKSdcWZNHAuwWHCTHnAhY7fT5nFZtFdrpdbebv2r/BCEXpZ/jTz0u7GTv7meRlWP7mby5lwecoqT3E+UUyeNk82mdOEyc+huE7aTZ5CghIePhWHCdM4x+ffjPp00b//H/KPefq7b2TXRG43ryFBD0I1UMJ7tI8+ULG/gx64IOnqjtgohWaqc8iwqQ3TutYjCWtaKaCE3RySD+u8LsU+JPoSNwHQwx84LXShfhvto09k0xqzqn903oVDnN1qU/Hmv+AVr2rOM2hNpzUVH8n5SDZR1neM67XffyBSyw20W2OkkIoDpsfiT1weoQeDnRVo0CBqa0dqOI3ccdS8daSdJqeka50yymlluNUKB6EcNmIYcwVKoBx3sW2YNkRcaMtka5EprQc2KNuxQQ1167BrrMzS3nwsHqYtGttxRa5mhkbHlBHIjKpr5uoGW6nHtpXjL5Zc0xc8G+zo5fJXLgyc68hpwaQSDVPGCNaZpmVD2w1m0B01NnO+pxho+sWo997GeZnHVFyXp/jNuo9ryP+92Bbpqw/s25bR3WyfKBY5Y/Qhr/c11vP5fP4BAAD//wMA//ibKjYEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFyoR2n7pu1VXbWd6FohvrCS+Xv6yXrWaueks8RrMsQ1ioYP9eg4IlkpJArV6FwTvi6sf2HgK6Rn76ial48UFz8H1rNPPuD8PfcGX7lpXrGK80T7sCQILov5Z7+7529vD48Ph8urhZZMrjyynoEdQHej4aSGlmsA4kM7ACeHRA2BrLW70HxIFANM1YLfXvvjnAIl16tK1ZXgyrJzyRASZMkDLPT485RVbBbdZbzc4O3Yv7BniqxX5S8zL+Vu3ORv3Pz0BAFvRh/8cmQlO8X5RDF52uzYJpwmSH4OxbXJbvIUEivZw4fiMEEa5/j0h0cfLqPv/8fk78AdfaBif8d6JiQprDvio5KG6w6BQ1NbbkxtRpLOtkqyku3mkHxY53Up9iHR17ipZD37G6aFLvpuozv6RC6tMUv/bHLW9MOn3YwZEKxkhzjj6lLxarcUlahqkS8f1nRaU/Ge0EdyifJMj3G9lv8PRRm1kXZrjBRScYB0LN7AcmQ9s9A5CRYsgHFupEbQKFCAES2SQUOoFbaorUajrXBGwyA1QiOHMSNZd0632BYKjdC21cglNQPXUozcggROgMKOrXCmzYxXW4uHaYvDdUIT1tzSiFxbCdzquuZYN9AqM7atGn+z5Jq4FNlvpOfL19gUGMSO0EiutGy4tlbyzjYtH9pusIPpqHFZwT3FQNNvRr3zLs7LPKbiujDFH9a9X0P+3sW2PN994D+2yO5m941ikSMHH/I2X1M+n8/nfwEAAP//AwAfRvRKJQQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUwW7cNhCGX0XgeSmTFEWJuhl2gi7c1IvESIveRpxRTFimFiTlxDD23Qtqt0h8KnohiBFn+M/3D/XGAjwTG9j18fjhhUJObMey30JKKMOF4UI9SDu03aCauuta06j2b7Zj/o5e2cA67Zx0lnhDhrhG0fKxmRxHJCuFRKFaXWrCt8SGNwa+RnrxjuolfaWY/BLYwP70AZfv5W7wtZuXFeu4zLQPKUNwRcwf+5s7/vv14eH+cD6VKJXk2iMbGNgRdD8ZTmrsuAYgPnYjcHJI1BLIRrtzmg+ZYoC5Tvj0835ccqDsBlWrphZcWXbaMYQMRfIIiR5ej0XFhui2xHdbeNsOb+yFIhvU7l+Y53JXbvZXz2lc/YxX5zVDekrbOoHLS3xlO3aMy5Fi9rTxuU6Jnsf59QHS08fzmbJNH36QWzPhzbKGzAYmtWQ7tgGaZ8h+CdVF483sKeTtY44+JO/+q1bx5/5LdZghT0t8fufHlzPm/f+g/Ancow9U7W/ZwIQkhU1PfFLScN0jcGgby41pzETS2U6VRm6WkH1YlzVV+5DpW9xaYgP7CHOis77r6B59JpfXWPr8yxTdh7jg6nL100spalE3QpSkNR/XXH0m9JFcptLEQ1wv9b6+T1FGbUk3a4wUcnWA/Fj9BumRDcxC7yRYsADGuYlaQZNAAUZ0SAYNoVbYobYajbbCGQ2j1AitHKcSYTtGZXSqbVrbroHWTJr3I3VcS7QcJBreCbI4WWnHtn/HsbqfN/6uF5qw4ZYm5NpK4FY3DcemhU6ZqevU9AuSyzxIUQAjvZzf3abAIPaERnKlZcu1tZL3tu342PWjHU1PrSua7ygGmn8B9cm7uKRlytVlQqp36D6vofw7qm1avvvAf2we3S7uiWJVPAYfylO52Ho6nU7/AAAA//8DAIN+pxyCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwY7cKBCGX8XibDyAMTa+jXp2ldYkO61klKz2hqkijeKBFsaTRKN+9xXujpLJLTdUUFV/fX/xQoJ5QjKS29Ppr2cMeSE1yX4LCSYUZYoy8SjYyMXY6UawXnWD+I/UxN/jdzKSXlrLrUbaokIqgXV0ap2lAKg548BEJ0tN83kh4wsxvgF89habuHzEtPgYyEg++QDxa+ltfGPnuEKT4oz7sGQTbBHzz353T9/eHh4fDpdXCy4lufFARtK1YHopGDWDslQ6ZHSYnKNKaQlKdpINcEnzIWMKZm4W+PKzP8QcMNtRNKJtGBWanGsCJpsieTILPn4/FRUborsSr7fwdhxfyDMmMor6B8xLuRs7+xsbn55MgBvng1+OpCanFE+YsscNxzbhPJvsY6iuTXazx5BJTR4+VIfZZBfT0ytGHy6j7/9g8nfGHn3Aan9HRsI4CmgHpE5wReUAhpqu1VSpVjnkVveCk5rsYsg+rHFdqn3I+DltKslI/jbzghd9t8kefUab11Sk/6uK1/jN512EEmCkJocUYbW5+ombs4Y1LSuXD2s+rbl6j+AT2oxlpse0Xsv/liKU2JJ2a0oYcnUw+Vi9McuRjESbwXKjjTZGWeuwY+gYMKNYD6hAIUgBPcgNi2ZWSTNxCabjkyuRoru4W20LNYAQnUNNBQhOJWigRrUTVQxagMG5blCvsFYPc8kzdmASoaUaHVCpuaFati2FtjO9UK7vhfsFydVxzgpvwOfL19gUKIABQXEqJO+o1JrTQXc9nfph0pMasLNF8z2mgPMvoN55m+ISXa6uC1O9Qvd+DeV7V9vyfPWBftssu4v2C6aqWG58KNt8dfl8Pp//BwAA//8DAJt2xo0lBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTQW/cNhCF/4owZ1EmKYoSdTPWKbpwEy+SRVr0RnFGMWGZWlCUk8DY/x5Qu0Hi3noRhAEf571vhq8Q7DNBD7en07sXCmmBEpLfSpJLzbhmXB4l74XsG1NJrnSnzb9Qgr+n79BDq5wTzhCrSRNTyBs21KNjiGQEF8hlo/Kd9ssC/StYXyG9eEfVvHymuPg5QA9/+4Dz19zb+spN84pVnCfahyXZ4LKZD/vdPfvr9nB8OFxOLbRkceURemhqtK2SnNlOO6ZG4qwbxpFpbRRq1Sje4UXmQ6IY7FQt+PSrP84pUHK9rGRdcSYNnEtAm2y2PNiFjt9P2cWG6C7Xy628/fav8EIReln+hHm57sZN/uZ5GVY/4c3lm+zylFOe4nyimDxtTLaY02STn0Nx7bSbPIUEJTx8Kg6TTeMcn9+A+nTJv/8f8Y+5+7tv5NZEuJvXkKAHoTSU8N66Rx+o2N9BD1yQxLojNkqhmerQMtvUhmld65GEM60UUMJuDsmHdV6XYh8SfYlbAOjhDzstdLF+G92jT+TSGnOqf3TehUOccXWp+MVf8IpXNedZtKbTmoqPhD6SS5TzHeN6ve8/EqnlJtqtMVJIxcGmx+JPuzxCD8Z2TlhjjbXauZEaTiNHbjVvkTRqQiWxRbUhMtxpZQeh0DZiGHMFSqA87mLbMMt1I0Sj2EiNY6rtJDMGFaPREklXt13TvuFYPEybznVcEdbM0IhMGWGZUXXNsG5sK/XYtnL8Dcl1+oJnwEgvl7eyOdCIHaEWTCrRMGWMYJ1pWja03WAG3VHjsud7ioGm30C99y7Oyzym4ro8xRt0H9eQ33uxLdJXH9i3bUZ3s3uiWOQZWx/yel/Hej6fzz8AAAD//wMAR26UADYEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1Hm/aI3w25Rw02ziI226NuIM4qFaKWFRG0aGP7vBbVbJO5b34SBzvDwmzN8ZRMcibXs9nT66UxTXlnN8rCXlFCOC8eFelailbq1sfFGhxjsX6xmwyN9Yy3zJiWZInFNjrhBYXmn+8QRKUohUShrSk/4vLL2lcHQIJ2HRM28/k7LOswTa9kfw4Tz13I2DE0a5w2bZR7pYVozTKmY+e3h7pH/ent4/ni4/LXSWsTNgMWqtjH2aHgM0HPjlePBoeGKgk0gAgQDF9kwZVomGJsVv3w/H+c8UU6tapRuBFeRvdUMIUOx3MFKz99OxcWO6L7U6728f7av7EwLa1X9L8xLu5s0Djfr1o10pvEEy0rLTZqPR5iQ1ey0zCda8kA7ljMt3bwOuRBNyUMIUmIAb6zHoK3SHrUgDMJbjBjBGderZG2PXYje9AaVNOjImM6zmu3gxhHyME/V1fvdONCUWc0+PlWHEXI/L8d36J8uRB/+B9Bim7VMC6ud92S01hBdpwShd8k51SUQvu8UyUTCRuOj9ELqqETU1vngEURMrGYfIL0ME1UP96xlQpJCHYj3SjpuAgIHqyN3TrueZIpeSVazu3nKw7TN21o9TJk+L/t9Wct+hnGly01vl/QyZEp5WwqEP10J42GZcUu5+h4AKRrRaCGKaMunLVefCIeFUqaC43nZrv3+I1FO7aK7bVloytUB8kv1C6wvrGURQpIQIQK4lHqygnqBApzwSA4doVHo0USDzkSRnIFOGgQru75UWM2o5K3aI05Sd0II5N75npvO9DwEQh5sTFo4DWD9O47Vx7HoIAVhCDWP1CM3UQKPRmuO2oJXrvde9T8guYZFigIY6XxZ1t2BQwyETnJlpOUmRslDtJ53PnSxc4FsKp4faZlo/AHUhyEt8zr3ubpmrXqH7tM2lQen2nP3dZj43/uM7uf0hZaqzBiGqezXdaxvNTsSrNtCx/29al/ZoSxX9bw/W8o3ToZQs6cMS95O17IJjXXGvr29vf0DAAD//wMARwDop/QEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjz+0M3w25R1029iBdp0duIHNlCtNKCojYNjP3vAbUbJO6tF0EYcGZePvMO38gEByQtuT0efznhlBdSkzxsIcGEocxQJvaCtVy22jeWScO4/ofUZHjEr6QlVoXAg0cq0SBVkWnayT7QGNFzxiMTWpWa8LKQ9o3A0EQ8DQGbefmEaRnmibTkr2GK85fSG4YmjPMamzSP+DAtGaZQxPz5cPdI/7jd7Z92l1MLLiW5GWKRKrX3fVTUO+ipssJQZ6KiAp0OwBw4BZe0YcqYJhibJX7+0T/OecIcWtEI2TAqPDnXJEKGIrmDBfdfj0XFhui+xOstvP22b+SEibSi/g7zUu4mjMNNno8jnnA8Qlow3YT5cIApkpoc03zElAfcsGw3HUfIwzxV12Z344BTJjV5eq52I+R+Tod3rJ4vCB7+B4ETpo60RDItjbWopJTgTScYRmuCMaILwGzfCeQBmfbKem4Zl14wL7WxzkZgPpCavIxzB+Pvy8bPC9a5yKwwAhV6oYzxgaF02GuLLIIxnXHBR+msUC46CR6A+w5t8CYUg3yA8DpMWD3ck5YwjiJKh7QX3FDlIlDQ0lNjpOmRB28FJzW5m6c8TOu8LtXDlPElbfxIS36FccELudsUXoeMIa+pQP3blGa7NMc15OqHAzhrWCMZK0lrPq65+ohxSBgyFrz7tF7r/SdFGLEl3a0p4ZSrHeTX6jdYXgsVcIGDL1c1IfSoGfYsMjDMRjTRYFQi2qi8ikZ5FoyCjqsImnd9iZCaYDFctXncg+5602uqZPkYgdRxzqhF7kSURnbaveNYPY0lD4JjCqOkHvtIledAvZKSRqnBCtNbK/qfkFzNx1kBHPF02dZNgYnRYTScCsU1Vd5z6ry2tLOu851xqEPR/IhpwvEnUB+GkOZl7nN19W71Dt3HdSovTrX5+Msw0X+3Gd3P4TOmqswYhqks2HWs55ocEJY14WF7sNo3sivbVe23d0vYxnDnavKcIeX1eA0r12ij9Pl8Pn8DAAD//wMAZQmTEfUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/jNhCG/4rAs6hQFEWJuqVOFzWy2Rgbd1v0NiJHDhGZNCgq2yDwf19QdhBnFz30Joz4zsczL/lKHOyRdOT6cPj9GV2cSE6iXUKccUmZpIxvOevKqqtV0TR1xcr2H5ITe4svpCON0LrUCmmFEqkwrKZ9NWhqDKqSlYbxWqScsJtI90rAFgafrcbCT98wTNY70pG/rDP+e6oNttCjn00R/IhrN0VwOjXzZb26pZ+vN9v7zenUhFMSF9akVqtaqcEIqloYqGi4pK00gnJsaw2shVbASWZdxOBgLCbz9F7f+Ogw6o4XvCoY5Yocc2IgQmq5hwm3L4fUxYLoJsXzJbx8dq/kGQPpeP4G85TuSo/2aj/1sx3N1eh3O+t2K+8Gu5sDxFQ4J4fgDxiixQXOb9ZBePnsd7uUj3yCcUKSky2GvXUwnn7c4ADzGB/8HBYyk3n6rzOkIzBHT3KykBzHpWx2HmY1WnTxF+1F4fuHbDNCHHzYf1jSw4n9+n+gvwP9aB1m6xvSEVYiN1WLdOClpKI1QKGuFJWykgOWWjW8JDlZeRetm/08ZWsXcXem9qG/66AfbUQd55Bm+lsms22CN7OO2fuCS1awomIsieZ4mGP2FY0NqCOmIbZhPuf7ScIlX0Sf7Ii/4FnNIaCL2QbiY/YHTI+kIwpaXYICBSC1HrBmODDDQLLGoDQSjeCmMUIJI4ViWgroS2GgLvshRUhOMJksW3zNjBiMrBQFUTMq+NDTViOjptZVrRhP4Q9ws/sx6UC3TKCpqMLBUKFKoEpUFTVVDQ2XQ9Pw4YLT2RAlS9QvTfjnhObspS8na7/NbvD5dI+XPqUxLRpZUi7KmgqlStqquqF90/aqly3WOk228m7ybyC/Yej9ZGN6RO6ss3sYfz7yvplbDA7Hi+3cWR385IeYnW2ZfdjX19mlVyxbLPrdOvrvYowbr58wZMlYYN3FMo/H4/EHAAAA//8DAHDfcRcMBQAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7UMBD9lcjnOLUdx4lzq7YgVgW6ohUgbrOeCWuROivHKaBq/x05uwjKjZv17Dfz5r3xMwvwSKxn18fjqycKaWYlS36FlFCGC8OFelCil7oXorJS61qpL6xk/pZ+sp612jnpLPGaDHGNouH7enAckawUEoVqdK4JX2fWPzPwFdKTd1RN80eKs58C69knH3D6nnuDr9w4LVjFaaRtmBMEl8W8325u+dvr3cPd7vxqpjmTK49Zat1YO6DmtoOB61YZ3hnUXFHXOBAddBrONB8SxQBjNeO3P/1xSoGS61Wl6kpwZdmpZAgJsuQ9zPTw85hVrBbdZLxc4fXYP7MniqxX5W8zz+Wu3Oiv3PT4CAGvBh/8fGAlO8bpSDF5Wu1YJxxHSH4KxaXJZvQUEivZ3X2xGyENU3x84dH9efTtf0z+DtzBByq2N6xnQpLCuiM+KGm47hA4NLXlxtRmIOlsqyQr2WYKyYdlWuZiGxJ9jatK1rPXMM501ncd3cEncmmJWfpnk7OmHz5tJsyAYCXbxQkXl4o/dktRiaoW+fJuScclFR8IfSSXKM/0EJdL+X8oyqiVtFlipJCKHaRD8QbmA+uZhc5JsGABjHMDNYIGgQKMaJEMGkKtsEVtNRpthTMa9lIjNHI/ZCTrzukW60KBA4GmG7hqasW1dIJ3lgyXRlmwlgwYfGFrcTeeeZ3QhDW3NCDXVgK3uq451g20ygxtq4a/LLkkLkX2G+np/DVWBQaxIzSSKy0brq2VvLNNy/dtt7d701HjsuZbioHGv4x6512c5mlIxWVhihfWfVhC/t7FujzffeA/1shuJveNYpEjBx/yNl9SPp1Op18AAAD//wMATZ4i4iUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUwW7bOhD8FYFnU6EoihJ1C5wWz8hrY7RGW/S24q4aIgplkFTaIPC/F5RdpDk9vAtBrDjL2ZmhXpiHR2I9uz4e3z2RT5FtWHJrSQqpudBcyIMUfaV6IUojtGpq8Z1tmLulZ9azVllbWUO8Jk1coWj4UI+WI5KpRIVCNir3hB+R9S8MXIn05CyVc/xCIbrZs559dR7nn/lucKWd5gXLME+08zGBt5nMx932lv97vT/c7c+nIsUMLh1mqnVjzIiKmw5GrlqpeadRcUldY0F00Ck4w5xPFDxMZcSH1/txTp6S7WUp61JwadhpwxASZMoDRDo8HzOLVaKbXN+s5XXbv7AnCqyXmz9inttd2cldPcZhcRNendcE8SGu6wg2zeGZbdgxzEcKydGqz3WM9DhMzweID+/PZ/I2vvtFdkmE23nxifWsqg3bsFWgaYLkZl9cOG4nRz6tH1NwPjr7X72yP3efi/0EaZzD4xs/Pp9l3v0PlT+AvXeeit0N65moSGLdER9lpbnqEDg0teFa13qkyppWVmzDtrNPzi/zEoudT/QjrCOxnr2HKdKZ33Ww9y6RTUvIc37Tmfc+zLjYVLx6WYlSlLUQGbSk45KKT4QukE2UhziE5dLvy1uI1HIFbZcQyKdiD+m++AfiPeuZgc5WYMAAaGtHagSNAgVo0SJp1IRKYovKKNTKCKsVDJVCaKphzBW2YZSjU6xpbQ3JVirBqTaKq7EZuaG25dQhiU4rlKu5rzoWd1PGge2EIqy5oRG5MhVwo+qaY91AK/XYtnL8S5JLHiqRBUZ6Or+7lYFG7Ah1xaWqGq6MqXhnmpYPbTeYQXfU2MzgloKn6S+hPjgb5jiPqbgkpHgj3afF539Hsablp/P81+rRzWwfKBTZY3A+P5WLrafT6fQbAAD//wMAYoXgq4IEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUXU/lNhD9K5FfGwd/xYnzRoFVrygf2nvVVn2b2BNwCc6V48AixH9fORcK7KpSX6JobM85c+bMPJMA90g6crzfnz1gSDMpSfJrSDChKdOUiZ1gHVcdY5XhSnFp/iYl8ef4RDrSKGu5NUglaqTKsZr2crDUOTScccdErXJOuJlJ90zAVw4fvMVqmv/AOPspkI786YObHjM2+MqO0+KqOI24CXOCYDOZy83JOf39+Hp3dX24NeOcH1feZaqyNmZwipoWBqoaoWmrnaIC29oCa6FVcHjmQ8IYYKxmd/eO76YUMNlOVEJWjApDXkriIEGm3MOMu6d9ZrFKdJrj5Rpef7tn8oCRdKJ8E/OQ7siO/uh+7hc/uqP1S0qyj9MeY/K4qnG1Ld5JcFaxSmjBGCnJBdhbH7DYnBZXYy4RbMsUOkkNDo4qw4EaJSV1soZG6KFpxEBKco4x4Pgh64W3cZqnIRWvIhefcLbHJ2cB+hEzxhcYZyQl2czbBMHBOAU8+4Z2SYdcu7jk45MpJB+WaZmLTUh4E+H1/O39yRIjhlRcQ7otfoP5lnTEQGs5GDAA2toBa4YDcww0axxqp9Ep4RqnjHJaGWa1gp4rBzXvhxwhJfk1a7hdrMV5fmezmmQcVw7Fa59ORo8hfZKRdIRxFE62SAfBNVWtAwq1NFRrqQfk1jSCv8HsIN5gIh0Z2r7vmbW9UIZbEAakG5h0ejBCNA2oQdaNElrXrAHrmkZw5WSDFuQgWStISa7j5Babfmy1XBtwHad/0KYsFemIrblpWl5LUdeybViPqm6g1oyJRjPkRtZONtK5QbWmbWBwChrUqh14z4Rwb/TPwo0PeOrn/QhPH3DbSlfyFwvQ8gHeRT1c/xLhHh+neHd5sHF1ebYjJfm6hLwRik22yKMP9JvOE32xjMnvbiOCQ3cxOfzZR5jHpVgnVOZmS4V0kMJRJaSggKDpgLbFFq1xLJM/newdxiI7DHzIU/VvsqttcRztrU9o0xIzv79WHlfb4nqENEzx/tMm+VDYjwJUQnKWO+Pw4bCNVo7auRad5lQoXlNlDKetqRvaN21vet1ibbNiV0vaL6n4is5HtGmt+NWLK+bJLdq7n8V4c8GrRTnLXtsettgq7f9aYi8luUeYl4j367bunskmBIwr8ulyGMVNuPDj6Ge0U3Az6ThXuhLC6FeG/3lPMFYxWdcvLy8v3wEAAP//AwC4eGIfGwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJRFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BSa6dooe9CaP5eO/NG/5AvTwAatDqePx0gj4MKEXBTiFKKMeEY0J3lDQ5awjJaJGXlNN/UYrsHbyhBlVMqVwJwAVwwEyTEreFUVhrEDnJNaEliz3l84CaH0jaTMPJKsjc8Bf4wboeNehv22v3GmdLm6nOjTrzroNNPwTZqwjm62Z9h7+struH7Zw1wBCLM6sj1KIUwmiGRS0NZhXluOaaYQp1qSSpZc3kXGb7AL6XXTbol8t87UIPQTU0o0VGMBXoPUVaBhkht3KA3dsxopgkuo3xdApPn80PdAKPGpqexZzb3ajO3hyGdrSdvgnSP0MwXh7g1fkXOMkOpejo3RF8sDBp8/CUXCDlJCMZ5ZQQlKJ7qfa2h2Rzmzx0kbBUNWGgCyzAaMxELrFgRYF1UcqKclNV1KAU3YHvobvqem+Vd4MzIVkkTz7MWbs+2H5045Bs+gDPXoa57rPsBkAp2o5tZ4f9I0j9tnOPY/zXj13ksh69hz4kWxn2yZ9y2KMGCVmrXAoppORKGSgJGKKJ5KTSwDUHzaiuNBNMcyaI4ky2OdOyzFsTIyhFD2M4juGs//crEDtvDwfQFwS7SeNtJ4Nx/rDR0AdrbFzNOWMyVNdNrJKl57qz0IcPIqMGkRyoLmrAhuYcs1pLLMtCYM4LbiBXoqL5LJixz+NPnYgxmhWmrGveijYnQE1VM1KZlmhlCiV02UJFK1LVUAtFSFuyvG6FIhrykunIzjs9qvCrFYppQY9jH68z2UTar7bH3zm7KLL1Ljjlurgw28HT2xDg8Lutrd3h6AYbIn+zrPbbAPeuj3+X+180W4pXLlxiEO8gmU5PStnSquW4NabFDEyBa2grXFaUGVrTquaR1a1TL+CTaDBp+2knZ089PCUrr/Y2gAqjj5P/mUjN6/x8PpmLHtnXT7u187A6HtMl+scpijQ3Oxvgw7vybYjpezdEFsGPVz56gs6ccV25ScNpfqlsDHKta9A8x5TlJWZC5LgWZYXbqm5Fy2so1cWtySNo60GFqd9unrYI+ztbrnywRqowxNv54tTkpsWeS8qvIK+29rN6Hn61ucVJS6ecRMc+ze/mZKD/9Wy+v7+//wcAAP//AwAXRjOgJwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9KwLPokxSFCXqZqxTdOEmXiSLtOhtxBnFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4ygI8E+vZ7en07oVCWljJkt9KSijDheFCHZXope6FqKzotLLNv6xk/p6+s5612jnpLPGaDHGNouFDPTqOSFYKiUI1OveELwvrXxn4CunFO6rm5TPFxc+B9exvH3D+mmeDr9w0r1jFeaJ9WBIEl8l82O/u+V+3h+PD4fJqoSWDK4+Zat1YO6LmtoOR61YZ3hnUXFHXOBAddBouMB8SxQBTteDT23ycU6DkelWpuhJcWXYuGUKCTHmAhY7fT5nFZtFdrpdbebv2r+yFIutV+dPMS7sbN/mb52VY/YQ3lzPB8pRVnuJ8opg8bZ5sMqcJkp9DcZ20mzyFxEr28Kk4TJDGOT7/ZtSni/79/5B/zNPffSO3JsLdvIbEeiZ1zUr2HtyjD1Ts71jPhCSFdUd8VNJw3SFwaGrLjanNSNLZVklWst0ckg/rvC7FPiT6EjcBrGd/wLTQhfptdI8+kUtrzKr+MXkXDnHG1aXizX8pKlHVQmTQmk5rKj4S+kguUdZ3jOu1338gyqgNtFtjpJCKA6TH4k9YHlnPLHROggULYJwbqRE0ChRgRItk0BBqhS1qq9FoK5zRMEiN0MhhzBVWMspxF9uGDSMSNcJy0cqRa6kUt7JtuGxtO1jROqkzkTcfi4cp48B1QhPW3NKIXFsJ3Oq65lg30Coztq0af7Hkmr4U2WCkl8tf2RgYxI7QSK60bLi2VvLONi0f2m6wg+mocZnzPcVA0y9Gvfcuzss8puK6PMVv1n1cQ/7vxbZIX33g37aM7mb3RLHIGYMPeb2vsZ7P5/MPAAAA//8DANBOlYk2BAAA

View File

@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -881,7 +882,8 @@ public sealed class TimelineService(
IPlotRepository plots,
ILookupRepository lookups,
IWarningRepository warnings,
ISceneDependencyRepository sceneDependencies) : ITimelineService
ISceneDependencyRepository sceneDependencies,
ILogger<TimelineService> logger) : ITimelineService
{
private static readonly string[] DefaultGraphMetrics = ["Overall Intensity", "Tension", "Emotional Weight", "Action"];
@ -890,29 +892,29 @@ public sealed class TimelineService(
var projectId = filter.ProjectID;
var bookId = filter.BookID;
var selectedSceneId = filter.SelectedSceneID;
var timeline = await timelineRepository.GetByProjectAsync(projectId, bookId);
var timeline = await TimeTimelineLoadAsync("dbo.Timeline_GetByProject", () => timelineRepository.GetByProjectAsync(projectId, bookId), projectId, bookId);
if (timeline.Project is null)
{
return null;
}
var allBooks = await books.ListByProjectAsync(projectId);
var lookupData = await lookups.GetAllAsync();
var assetLookupData = await assets.GetLookupsAsync();
var characterLookupData = await characters.GetLookupsAsync();
var locationLookupData = await locations.GetLookupsAsync();
var plotLines = await plots.ListPlotLinesAsync(projectId);
var plotThreads = await plots.ListPlotThreadsByProjectAsync(projectId);
var allAssets = await assets.ListAssetsAsync(projectId);
var allCharacters = await characters.ListCharactersAsync(projectId);
var allLocations = await locations.ListByProjectAsync(projectId);
var warningLookups = await warnings.GetLookupsAsync();
var allBooks = await TimeTimelineLoadAsync("dbo.Book_ListByProject", () => books.ListByProjectAsync(projectId), projectId, bookId);
var lookupData = await TimeTimelineLoadAsync("dbo.Lookup_All", lookups.GetAllAsync, projectId, bookId);
var assetLookupData = await TimeTimelineLoadAsync("dbo.AssetLookup_All", assets.GetLookupsAsync, projectId, bookId);
var characterLookupData = await TimeTimelineLoadAsync("dbo.CharacterLookup_All", characters.GetLookupsAsync, projectId, bookId);
var locationLookupData = await TimeTimelineLoadAsync("dbo.LocationLookup_All", locations.GetLookupsAsync, projectId, bookId);
var plotLines = await TimeTimelineLoadAsync("dbo.PlotLine_ListByProject", () => plots.ListPlotLinesAsync(projectId), projectId, bookId);
var plotThreads = await TimeTimelineLoadAsync("dbo.PlotThread_ListByProject", () => plots.ListPlotThreadsByProjectAsync(projectId), projectId, bookId);
var allAssets = await TimeTimelineLoadAsync("dbo.StoryAsset_ListByProject", () => assets.ListAssetsAsync(projectId), projectId, bookId);
var allCharacters = await TimeTimelineLoadAsync("dbo.Character_ListByProject", () => characters.ListCharactersAsync(projectId), projectId, bookId);
var allLocations = await TimeTimelineLoadAsync("dbo.Location_ListByProject", () => locations.ListByProjectAsync(projectId), projectId, bookId);
var warningLookups = await TimeTimelineLoadAsync("dbo.WarningLookup_All", warnings.GetLookupsAsync, projectId, bookId);
var chaptersByBook = timeline.Chapters.GroupBy(x => x.BookID).ToDictionary(x => x.Key, x => x.ToList());
var scenesByChapter = timeline.Scenes.GroupBy(x => x.ChapterID).ToDictionary(x => x.Key, x => x.ToList());
var purposeLabelsByScene = timeline.PurposeLabels.GroupBy(x => x.SceneID).ToDictionary(x => x.Key, x => x.OrderBy(p => p.SortOrder).ToList());
var orderedScenes = timeline.Scenes.ToList();
var warningCounts = (await warnings.GetSceneCountsAsync(projectId, bookId)).ToDictionary(x => x.SceneID, x => x.WarningCount);
var dependencyCounts = (await sceneDependencies.GetCountsAsync(projectId, bookId)).ToDictionary(x => x.SceneID, x => x.DependencyCount);
var warningCounts = (await TimeTimelineLoadAsync("dbo.ContinuityWarning_SceneCounts", () => warnings.GetSceneCountsAsync(projectId, bookId), projectId, bookId)).ToDictionary(x => x.SceneID, x => x.WarningCount);
var dependencyCounts = (await TimeTimelineLoadAsync("dbo.SceneDependency_Counts", () => sceneDependencies.GetCountsAsync(projectId, bookId), projectId, bookId)).ToDictionary(x => x.SceneID, x => x.DependencyCount);
foreach (var scene in orderedScenes)
{
@ -921,16 +923,18 @@ public sealed class TimelineService(
scene.DependencyCount = dependencyCounts.GetValueOrDefault(scene.SceneID);
}
var assetTimeline = await assets.GetTimelineAsync(projectId, bookId);
var characterTimeline = await characters.GetTimelineAsync(projectId, bookId);
var assetTimeline = await TimeTimelineLoadAsync("dbo.AssetTimeline_GetByProject", () => assets.GetTimelineAsync(projectId, bookId), projectId, bookId);
var characterTimeline = await TimeTimelineLoadAsync("dbo.CharacterTimeline_GetByProject", () => characters.GetTimelineAsync(projectId, bookId), projectId, bookId);
var scenePovLabels = characterTimeline.Appearances
.Where(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)
.GroupBy(x => x.SceneID)
.ToDictionary(x => x.Key, x => x.First().CharacterName);
var matchingSceneIds = await BuildMatchingSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId);
var focusSceneIds = await BuildFocusSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId);
var matchingSceneIds = await TimeTimelineLoadAsync("TimelineService.BuildMatchingSceneIds", () => BuildMatchingSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId), projectId, bookId);
var focusSceneIds = await TimeTimelineLoadAsync("TimelineService.BuildFocusSceneIds", () => BuildFocusSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId), projectId, bookId);
var focusLabel = BuildFocusLabel(filter, plotLines, plotThreads, allAssets, allCharacters, allLocations);
var selectedScene = selectedSceneId.HasValue ? await scenes.GetEditAsync(selectedSceneId.Value) : null;
var selectedScene = selectedSceneId.HasValue
? await TimeTimelineLoadAsync("SceneService.GetEditAsync", () => scenes.GetEditAsync(selectedSceneId.Value), projectId, bookId)
: null;
if (selectedScene is not null)
{
selectedScene.ReturnProjectID = projectId;
@ -974,7 +978,7 @@ public sealed class TimelineService(
WarningTypeOptions = warningLookups.WarningTypes.Select(x => new SelectListItem(x.TypeName, x.WarningTypeID.ToString(), x.WarningTypeID == filter.WarningTypeID)).ToList(),
RevisionStatusOptions = lookupData.RevisionStatuses.Select(x => new SelectListItem(x.StatusName, x.RevisionStatusID.ToString(), x.RevisionStatusID == filter.RevisionStatusID)).ToList(),
ScenePurposeOptions = lookupData.ScenePurposeTypes.Select(x => new SelectListItem(x.PurposeName, x.ScenePurposeTypeID.ToString(), x.ScenePurposeTypeID == filter.ScenePurposeTypeID)).ToList(),
ViewPresets = await presets.ListAsync(projectId, bookId),
ViewPresets = await TimeTimelineLoadAsync("dbo.TimelineViewPreset_List", () => presets.ListAsync(projectId, bookId), projectId, bookId),
Books = timeline.Books.Select(book => new BookTimelineViewModel
{
Book = book,
@ -1002,6 +1006,25 @@ public sealed class TimelineService(
public Task ArchivePresetAsync(int timelineViewPresetId) => presets.ArchiveAsync(timelineViewPresetId);
private async Task<T> TimeTimelineLoadAsync<T>(string operationName, Func<Task<T>> operation, int projectId, int? bookId)
{
var stopwatch = Stopwatch.StartNew();
try
{
return await operation();
}
finally
{
stopwatch.Stop();
logger.LogInformation(
"Timeline load operation {OperationName} completed in {ElapsedMilliseconds} ms for ProjectID {ProjectID}, BookID {BookID}.",
operationName,
stopwatch.ElapsedMilliseconds,
projectId,
bookId);
}
}
public async Task<ContinuityValidationSummary?> MoveSceneByDropAsync(TimelineSceneDropMoveViewModel model)
{
if (model.SceneID == model.AnchorSceneID)
@ -1188,14 +1211,14 @@ public sealed class TimelineService(
if (filter.CharacterID.HasValue) Intersect(characterAppearances.Where(x => x.CharacterID == filter.CharacterID).Select(x => x.SceneID));
if (filter.LocationID.HasValue)
{
var locationScenes = (await locations.ListScenesByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
var characterLocationScenes = (await locations.ListCharactersByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
var assetLocationScenes = (await locations.ListAssetsByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
var locationScenes = (await TimeTimelineLoadAsync("dbo.LocationScene_ListByLocation", () => locations.ListScenesByLocationAsync(filter.LocationID.Value), projectId, bookId)).Select(x => x.SceneID);
var characterLocationScenes = (await TimeTimelineLoadAsync("dbo.LocationCharacter_ListByLocation", () => locations.ListCharactersByLocationAsync(filter.LocationID.Value), projectId, bookId)).Select(x => x.SceneID);
var assetLocationScenes = (await TimeTimelineLoadAsync("dbo.SceneAssetLocation_ListByLocation", () => locations.ListAssetsByLocationAsync(filter.LocationID.Value), projectId, bookId)).Select(x => x.SceneID);
Intersect(locationScenes.Concat(characterLocationScenes).Concat(assetLocationScenes));
}
if (filter.WarningSeverityID.HasValue || filter.WarningTypeID.HasValue || filter.ShowWarningsOnly)
{
var warningScenes = (await warnings.ListAsync(projectId, bookId, null, filter.WarningTypeID, filter.WarningSeverityID, null, filter.IncludeDismissedWarnings, filter.IncludeIntentionalWarnings))
var warningScenes = (await TimeTimelineLoadAsync("dbo.ContinuityWarning_List", () => warnings.ListAsync(projectId, bookId, null, filter.WarningTypeID, filter.WarningSeverityID, null, filter.IncludeDismissedWarnings, filter.IncludeIntentionalWarnings), projectId, bookId))
.Where(x => x.SceneID.HasValue)
.Select(x => x.SceneID!.Value);
Intersect(warningScenes);
@ -1225,9 +1248,9 @@ public sealed class TimelineService(
"asset" => assetEvents.Where(x => x.StoryAssetID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
"plotline" => threadEvents.Where(x => x.PlotLineID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
"plotthread" => threadEvents.Where(x => x.PlotThreadID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
"location" => (await locations.ListScenesByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID)
.Concat((await locations.ListCharactersByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID))
.Concat((await locations.ListAssetsByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID))
"location" => (await TimeTimelineLoadAsync("dbo.LocationScene_ListByLocation", () => locations.ListScenesByLocationAsync(filter.FocusID.Value), projectId, bookId)).Select(x => x.SceneID)
.Concat((await TimeTimelineLoadAsync("dbo.LocationCharacter_ListByLocation", () => locations.ListCharactersByLocationAsync(filter.FocusID.Value), projectId, bookId)).Select(x => x.SceneID))
.Concat((await TimeTimelineLoadAsync("dbo.SceneAssetLocation_ListByLocation", () => locations.ListAssetsByLocationAsync(filter.FocusID.Value), projectId, bookId)).Select(x => x.SceneID))
.ToHashSet(),
"scene" => orderedScenes.Where(x => x.SceneID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
_ => new HashSet<int>()

View File

@ -0,0 +1,142 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
/*
Timeline performance pass for imported/large projects.
Focus:
- Keep indexes narrow.
- Support the actual timeline/project load path.
- Avoid duplicating existing core indexes on Books, Chapters, Scenes,
PlotLines, StoryAssets, SceneCharacters(SceneID), AssetEvents(SceneID),
and ContinuityWarnings(ProjectID, BookID, SceneID...).
*/
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ThreadEvents_PlotThreadID_SceneID' AND object_id = OBJECT_ID(N'dbo.ThreadEvents'))
BEGIN
-- Supports plot-thread filters and thread-event reads starting from a thread.
CREATE INDEX IX_ThreadEvents_PlotThreadID_SceneID
ON dbo.ThreadEvents(PlotThreadID, SceneID, EventTypeID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_StoryAssetID_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetEvents'))
BEGIN
-- Supports asset focus/filter lanes and asset-event reads starting from an asset.
CREATE INDEX IX_AssetEvents_StoryAssetID_SceneID
ON dbo.AssetEvents(StoryAssetID, SceneID, AssetEventTypeID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_CharacterID_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneCharacters'))
BEGIN
-- Supports character focus/filter lanes and character detail appearance lookups.
CREATE INDEX IX_SceneCharacters_CharacterID_SceneID
ON dbo.SceneCharacters(CharacterID, SceneID, RoleInSceneTypeID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Characters_ProjectID_Importance_Name' AND object_id = OBJECT_ID(N'dbo.Characters'))
BEGIN
-- Supports CharacterTimeline_GetByProject ordering by importance, then name.
CREATE INDEX IX_Characters_ProjectID_Importance_Name
ON dbo.Characters(ProjectID, IsArchived, CharacterImportance DESC, CharacterName);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_Project_Parent_Name' AND object_id = OBJECT_ID(N'dbo.Locations'))
BEGIN
-- Supports project location lists and LocationPaths roots by project.
CREATE INDEX IX_Locations_Project_Parent_Name
ON dbo.Locations(ProjectID, IsArchived, ParentLocationID, LocationName);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_ParentLocationID' AND object_id = OBJECT_ID(N'dbo.Locations'))
BEGIN
-- Supports recursive LocationPaths expansion from parent to child locations.
CREATE INDEX IX_Locations_ParentLocationID
ON dbo.Locations(ParentLocationID, IsArchived, LocationName);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_LocationID_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneCharacters'))
BEGIN
-- Supports timeline location filters that include character appearances at a location.
CREATE INDEX IX_SceneCharacters_LocationID_SceneID
ON dbo.SceneCharacters(LocationID, SceneID, CharacterID)
WHERE LocationID IS NOT NULL;
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneAssetLocations_LocationID_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneAssetLocations'))
BEGIN
-- Supports timeline location filters that include assets placed at a location.
CREATE INDEX IX_SceneAssetLocations_LocationID_SceneID
ON dbo.SceneAssetLocations(LocationID, SceneID, StoryAssetID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneAssetLocations_SceneID_LocationID' AND object_id = OBJECT_ID(N'dbo.SceneAssetLocations'))
BEGIN
-- Supports scene-level location/asset reads from the scene inspector and timeline focus.
CREATE INDEX IX_SceneAssetLocations_SceneID_LocationID
ON dbo.SceneAssetLocations(SceneID, LocationID, StoryAssetID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ContinuityWarnings_ActiveSceneCounts' AND object_id = OBJECT_ID(N'dbo.ContinuityWarnings'))
BEGIN
-- Supports active warning counts shown on timeline scene cards.
CREATE INDEX IX_ContinuityWarnings_ActiveSceneCounts
ON dbo.ContinuityWarnings(ProjectID, BookID, SceneID)
WHERE SceneID IS NOT NULL AND IsDismissed = 0 AND IsIntentional = 0;
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneDependencies_Project_SourceScene' AND object_id = OBJECT_ID(N'dbo.SceneDependencies'))
BEGIN
-- Supports dependency counts and source-scene dependency lookups.
CREATE INDEX IX_SceneDependencies_Project_SourceScene
ON dbo.SceneDependencies(ProjectID, IsArchived, SourceSceneID, TargetSceneID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneDependencies_Project_TargetScene' AND object_id = OBJECT_ID(N'dbo.SceneDependencies'))
BEGIN
-- Supports dependency counts and target-scene dependency lookups.
CREATE INDEX IX_SceneDependencies_Project_TargetScene
ON dbo.SceneDependencies(ProjectID, IsArchived, TargetSceneID, SourceSceneID);
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_Counts
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
;WITH DependencyScenes AS
(
SELECT sd.SceneDependencyID, sd.SourceSceneID AS SceneID
FROM dbo.SceneDependencies sd
WHERE sd.ProjectID = @ProjectID AND sd.IsArchived = 0
UNION ALL
SELECT sd.SceneDependencyID, sd.TargetSceneID AS SceneID
FROM dbo.SceneDependencies sd
WHERE sd.ProjectID = @ProjectID
AND sd.IsArchived = 0
AND sd.TargetSceneID <> sd.SourceSceneID
)
SELECT scene.SceneID, COUNT(*) AS DependencyCount
FROM dbo.NarrativeSceneOrder scene
INNER JOIN DependencyScenes dependencyScene ON dependencyScene.SceneID = scene.SceneID
WHERE scene.ProjectID = @ProjectID AND (@BookID IS NULL OR scene.BookID = @BookID)
GROUP BY scene.SceneID;
END;
GO

View File

@ -0,0 +1,151 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
/*
Focused optimisation for dbo.CharacterTimeline_GetByProject.
Diagnosis:
- The timeline timing showed this procedure as the clear bottleneck.
- The original appearance query joined SceneCharacters to Characters, Scenes,
Chapters, Books, and the recursive LocationPaths view three times.
- Filtering the project/book scene set once gives SQL Server a small driving
set before joining appearances.
- Materialising project location paths once avoids repeated recursive view
expansion for location, entry location, and exit location labels.
*/
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Characters_ProjectID_Importance_Name' AND object_id = OBJECT_ID(N'dbo.Characters'))
BEGIN
-- Supports CharacterTimeline_GetByProject character list ordering.
CREATE INDEX IX_Characters_ProjectID_Importance_Name
ON dbo.Characters(ProjectID, IsArchived, CharacterImportance DESC, CharacterName);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_CharacterID_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneCharacters'))
BEGIN
-- Supports appearance lookups when the optimizer starts from project characters.
CREATE INDEX IX_SceneCharacters_CharacterID_SceneID
ON dbo.SceneCharacters(CharacterID, SceneID, RoleInSceneTypeID);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_Project_Parent_Name' AND object_id = OBJECT_ID(N'dbo.Locations'))
BEGIN
-- Supports project-scoped LocationPaths materialisation.
CREATE INDEX IX_Locations_Project_Parent_Name
ON dbo.Locations(ProjectID, IsArchived, ParentLocationID, LocationName);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_ParentLocationID' AND object_id = OBJECT_ID(N'dbo.Locations'))
BEGIN
-- Supports recursive LocationPaths expansion from parent to child locations.
CREATE INDEX IX_Locations_ParentLocationID
ON dbo.Locations(ParentLocationID, IsArchived, LocationName);
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #TimelineScenes
(
SceneID int NOT NULL PRIMARY KEY,
SceneNumber decimal(9,2) NOT NULL,
SceneTitle nvarchar(200) NOT NULL,
ChapterNumber decimal(9,2) NOT NULL,
BookTitle nvarchar(200) NOT NULL,
BookSortOrder int NOT NULL,
ChapterSortOrder int NOT NULL,
SceneSortOrder int NOT NULL
);
INSERT #TimelineScenes (SceneID, SceneNumber, SceneTitle, ChapterNumber, BookTitle, BookSortOrder, ChapterSortOrder, SceneSortOrder)
SELECT s.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle, b.SortOrder, ch.SortOrder, s.SortOrder
FROM dbo.Books b
INNER JOIN dbo.Chapters ch ON ch.BookID = b.BookID
INNER JOIN dbo.Scenes s ON s.ChapterID = ch.ChapterID
WHERE b.ProjectID = @ProjectID
AND b.IsArchived = 0
AND ch.IsArchived = 0
AND s.IsArchived = 0
AND (@BookID IS NULL OR b.BookID = @BookID);
CREATE TABLE #TimelineCharacters
(
CharacterID int NOT NULL PRIMARY KEY,
ProjectID int NOT NULL,
CharacterName nvarchar(200) NOT NULL,
ShortName nvarchar(100) NULL,
Sex nvarchar(50) NULL,
BirthDate date NULL,
AgeAtSeriesStart int NULL,
AgeReferenceSceneID int NULL,
Height nvarchar(50) NULL,
EyeColour nvarchar(50) NULL,
CharacterImportance int NULL,
DefaultDescription nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL,
UpdatedDate datetime2 NOT NULL,
IsArchived bit NOT NULL
);
INSERT #TimelineCharacters (CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived)
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0;
CREATE TABLE #ProjectLocationPaths
(
LocationID int NOT NULL PRIMARY KEY,
LocationName nvarchar(200) NOT NULL,
LocationPath nvarchar(max) NOT NULL
);
;WITH ProjectLocationTree AS
(
SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath
FROM dbo.Locations
WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0
UNION ALL
SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath
FROM dbo.Locations child
INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID
WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0
)
INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath)
SELECT LocationID, LocationName, LocationPath
FROM ProjectLocationTree;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM #TimelineCharacters
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, s.ChapterNumber, s.BookTitle,
sc.CreatedDate, sc.UpdatedDate
FROM #TimelineScenes s
INNER JOIN dbo.SceneCharacters sc ON sc.SceneID = s.SceneID
INNER JOIN #TimelineCharacters c ON c.CharacterID = sc.CharacterID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN #ProjectLocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN #ProjectLocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
ORDER BY s.BookSortOrder, s.ChapterSortOrder, s.SceneSortOrder, c.CharacterName;
END;
GO

View File

@ -1,9 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="light" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - PlotLine</title>
<script>
(() => {
let savedTheme = null;
try {
savedTheme = localStorage.getItem("plotline.theme");
} catch {
savedTheme = null;
}
const theme = savedTheme === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = theme;
document.documentElement.dataset.bsTheme = theme;
})();
</script>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
@ -12,7 +25,7 @@
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom plotline-nav mb-3">
<nav class="navbar navbar-expand-sm navbar-toggleable-sm border-bottom plotline-nav mb-3">
<div class="container-fluid">
<a class="navbar-brand fw-semibold" asp-area="" asp-controller="Projects" asp-action="Index">PlotLine</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
@ -34,6 +47,10 @@
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">About</a>
</li>
</ul>
<div class="theme-switcher btn-group btn-group-sm" role="group" aria-label="Theme selection">
<button type="button" class="btn btn-outline-secondary" data-theme-choice="light" aria-pressed="true">Light</button>
<button type="button" class="btn btn-outline-secondary" data-theme-choice="dark" aria-pressed="false">Dark</button>
</div>
</div>
</div>
</nav>

View File

@ -27,6 +27,34 @@
--plotline-soft: var(--plotline-accent-soft);
}
[data-theme="dark"] {
color-scheme: dark;
--plotline-bg: #14110f;
--plotline-bg-warm: #1c1714;
--plotline-surface: rgba(35, 29, 25, 0.96);
--plotline-surface-elevated: #2a231e;
--plotline-border-soft: rgba(231, 214, 190, 0.16);
--plotline-text: #f1e7d8;
--plotline-text-muted: #b9aa99;
--plotline-accent: #d49a62;
--plotline-accent-dark: #f0c99d;
--plotline-accent-soft: #3a2a20;
--plotline-secondary: #78b9c2;
--plotline-secondary-soft: #21383d;
--plotline-danger: #ee8e83;
--plotline-warning: #e0ad65;
--plotline-success: #91c99a;
--plotline-timeline-bg: #211b17;
--plotline-shadow-card: 0 16px 38px rgba(0, 0, 0, 0.34);
--plotline-shadow-subtle: 0 10px 26px rgba(0, 0, 0, 0.24);
--plotline-ink: var(--plotline-text);
--plotline-muted: var(--plotline-text-muted);
--plotline-paper: var(--plotline-bg-warm);
--plotline-panel: var(--plotline-surface);
--plotline-line: var(--plotline-border-soft);
--plotline-soft: var(--plotline-accent-soft);
}
body {
color: var(--plotline-text);
background:
@ -851,6 +879,237 @@ a:focus-visible {
line-height: 1.55;
}
.theme-switcher {
flex: 0 0 auto;
}
.theme-switcher .btn {
--bs-btn-color: var(--plotline-text-muted);
--bs-btn-border-color: var(--plotline-border-soft);
--bs-btn-hover-color: var(--plotline-accent-dark);
--bs-btn-hover-bg: rgba(122, 77, 44, 0.1);
--bs-btn-hover-border-color: rgba(122, 77, 44, 0.28);
--bs-btn-active-color: #ffffff;
--bs-btn-active-bg: var(--plotline-accent);
--bs-btn-active-border-color: var(--plotline-accent);
font-weight: 700;
}
[data-theme="dark"] body {
background:
radial-gradient(circle at top left, rgba(212, 154, 98, 0.12), transparent 34rem),
linear-gradient(135deg, var(--plotline-bg-warm), var(--plotline-bg) 52%, #0f0d0c);
}
[data-theme="dark"] .plotline-nav {
background: rgba(29, 23, 20, 0.92) !important;
border-bottom-color: var(--plotline-border-soft) !important;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.24);
}
[data-theme="dark"] .navbar-toggler {
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .page-heading h1,
[data-theme="dark"] .workspace-heading h1,
[data-theme="dark"] .timeline-scene-card strong,
[data-theme="dark"] .timeline-chapter-title strong,
[data-theme="dark"] .timeline-book-title strong,
[data-theme="dark"] .plain-card h2,
[data-theme="dark"] .plain-card h3,
[data-theme="dark"] .context-title strong,
[data-theme="dark"] .asset-card h2,
[data-theme="dark"] .character-card h2 {
color: var(--plotline-text);
}
[data-theme="dark"] .plain-card,
[data-theme="dark"] .empty-panel,
[data-theme="dark"] .edit-panel,
[data-theme="dark"] .list-section,
[data-theme="dark"] .story-bible-section,
[data-theme="dark"] .story-bible-card,
[data-theme="dark"] .archive-item-card,
[data-theme="dark"] .warning-card,
[data-theme="dark"] .warning-mini-card,
[data-theme="dark"] .scenario-chapter,
[data-theme="dark"] .drag-drop-panel,
[data-theme="dark"] .import-tree-book,
[data-theme="dark"] .import-confirmation,
[data-theme="dark"] .import-docs details,
[data-theme="dark"] .scene-inspector-panel,
[data-theme="dark"] .pacing-panel,
[data-theme="dark"] .timeline-control-panel,
[data-theme="dark"] .timeline-drag-palette,
[data-theme="dark"] .timeline-filter-drawer,
[data-theme="dark"] .timeline-control-group,
[data-theme="dark"] .timeline-visibility-panel,
[data-theme="dark"] .timeline-jump-panel,
[data-theme="dark"] .timeline-preset-panel,
[data-theme="dark"] .attachment-card,
[data-theme="dark"] .attachment-preview-row,
[data-theme="dark"] .asset-mini-item,
[data-theme="dark"] .archive-empty {
color: var(--plotline-text);
background: var(--plotline-surface);
border-color: var(--plotline-border-soft);
box-shadow: var(--plotline-shadow-subtle);
}
[data-theme="dark"] .timeline-workspace,
[data-theme="dark"] .timeline-shell {
background:
linear-gradient(180deg, rgba(42, 35, 30, 0.96), rgba(29, 24, 21, 0.96)),
var(--plotline-timeline-bg);
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .timeline-section-header,
[data-theme="dark"] .lane-board-heading,
[data-theme="dark"] .plot-lane-board-heading,
[data-theme="dark"] .pacing-heading,
[data-theme="dark"] .lane-group-heading,
[data-theme="dark"] .timeline-chapter-title,
[data-theme="dark"] .timeline-book-title {
color: var(--plotline-accent-dark);
background: rgba(37, 31, 27, 0.96);
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .timeline-project-title {
background: #352318;
}
[data-theme="dark"] .timeline-chapter,
[data-theme="dark"] .timeline-scene-card,
[data-theme="dark"] .timeline-empty-scene,
[data-theme="dark"] .plot-lane-label,
[data-theme="dark"] .asset-lane-label,
[data-theme="dark"] .character-lane-label,
[data-theme="dark"] .plot-lane-slot,
[data-theme="dark"] .asset-lane-slot,
[data-theme="dark"] .character-lane-slot,
[data-theme="dark"] .timeline-placeholder-scene,
[data-theme="dark"] .timeline-placeholder-cell,
[data-theme="dark"] .metric-graph-points,
[data-theme="dark"] .timeline-minimap {
color: var(--plotline-text);
background: rgba(42, 35, 30, 0.9);
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .timeline-scene-card,
[data-theme="dark"] .timeline-empty-scene {
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
}
[data-theme="dark"] .timeline-scene-card:hover,
[data-theme="dark"] .timeline-scene-card.selected,
[data-theme="dark"] .timeline-scene-card.focus-match,
[data-theme="dark"] .timeline-scene-card.keyboard-focus,
[data-theme="dark"] .timeline-scene-card.search-hit {
border-color: rgba(212, 154, 98, 0.62);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.28);
}
[data-theme="dark"] .timeline-minimap button {
color: var(--plotline-accent-dark);
background: linear-gradient(180deg, #332920 0%, #241d18 100%);
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .timeline-minimap button.is-active {
color: #1c120d;
background: linear-gradient(180deg, var(--plotline-accent-dark) 0%, var(--plotline-accent) 100%);
}
[data-theme="dark"] .drag-chip,
[data-theme="dark"] .purpose-chip,
[data-theme="dark"] .metric-mini-summary,
[data-theme="dark"] .scene-pov-label,
[data-theme="dark"] .location-pill,
[data-theme="dark"] .archive-pill,
[data-theme="dark"] .badge,
[data-theme="dark"] .status-pill,
[data-theme="dark"] .scenario-tag,
[data-theme="dark"] .plain-preview {
color: var(--plotline-text);
background: rgba(212, 154, 98, 0.14);
border-color: rgba(212, 154, 98, 0.24);
}
[data-theme="dark"] .form-control,
[data-theme="dark"] .form-select,
[data-theme="dark"] textarea,
[data-theme="dark"] input,
[data-theme="dark"] select {
color: var(--plotline-text);
background-color: #211b17;
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .form-control::placeholder,
[data-theme="dark"] textarea::placeholder,
[data-theme="dark"] input::placeholder {
color: #8f8172;
}
[data-theme="dark"] .form-control:focus,
[data-theme="dark"] .form-select:focus,
[data-theme="dark"] .form-check-input:focus {
border-color: var(--plotline-accent);
box-shadow: 0 0 0 0.2rem rgba(212, 154, 98, 0.2);
}
[data-theme="dark"] .table {
--bs-table-color: var(--plotline-text);
--bs-table-bg: transparent;
--bs-table-border-color: var(--plotline-border-soft);
--bs-table-striped-color: var(--plotline-text);
--bs-table-striped-bg: rgba(255, 255, 255, 0.035);
--bs-table-hover-color: var(--plotline-text);
--bs-table-hover-bg: rgba(212, 154, 98, 0.1);
}
[data-theme="dark"] .modal-content,
[data-theme="dark"] .plotline-confirm-modal {
color: var(--plotline-text);
background: var(--plotline-surface-elevated);
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .modal-header,
[data-theme="dark"] .modal-footer {
border-color: var(--plotline-border-soft);
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(1) brightness(1.6);
}
[data-theme="dark"] .dropdown-menu {
--bs-dropdown-color: var(--plotline-text);
--bs-dropdown-bg: var(--plotline-surface-elevated);
--bs-dropdown-border-color: var(--plotline-border-soft);
--bs-dropdown-link-color: var(--plotline-text);
--bs-dropdown-link-hover-color: var(--plotline-accent-dark);
--bs-dropdown-link-hover-bg: rgba(212, 154, 98, 0.12);
}
[data-theme="dark"] .alert-info {
--bs-alert-color: #cceaf0;
--bs-alert-bg: #17343a;
--bs-alert-border-color: #285c66;
}
[data-theme="dark"] .alert-warning,
[data-theme="dark"] .validation-summary-errors {
--bs-alert-color: #ffe3ad;
--bs-alert-bg: #3b2a16;
--bs-alert-border-color: #705229;
}
@media (max-width: 1200px) {
.timeline-filter-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));

View File

@ -1,3 +1,42 @@
(() => {
const storageKey = "plotline.theme";
const choices = document.querySelectorAll("[data-theme-choice]");
const readTheme = () => {
try {
return localStorage.getItem(storageKey);
} catch {
return null;
}
};
const saveTheme = (theme) => {
try {
localStorage.setItem(storageKey, theme);
} catch {
// Theme still applies for the current page if persistence is unavailable.
}
};
const applyTheme = (theme) => {
const nextTheme = theme === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = nextTheme;
document.documentElement.dataset.bsTheme = nextTheme;
saveTheme(nextTheme);
choices.forEach((button) => {
const isActive = button.dataset.themeChoice === nextTheme;
button.classList.toggle("active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
};
choices.forEach((button) => {
button.addEventListener("click", () => applyTheme(button.dataset.themeChoice));
});
applyTheme(readTheme() || "light");
})();
(() => {
const modalElement = document.getElementById("plotlineConfirmModal");
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;