mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
Merge pull request #948 from tdurieux/feat/ui-polish
feat(ui): one consistent design across author, reviewer and admin screens
This commit is contained in:
43 files changed
+7880
-5379
No files matched your search
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"core.min.js": "core.d23af1c652.min.js",
|
||||
"vendor.min.js": "vendor.d5b4d04371.min.js",
|
||||
"vendor.min.js": "vendor.ad23251845.min.js",
|
||||
"mermaid.min.js": "mermaid.a7356d5ed1.min.js",
|
||||
"all.min.css": "all.8c4e8ecba6.min.css",
|
||||
"all.min.css": "all.b752450906.min.css",
|
||||
"markdown.min.js": "markdown.6c8dfb7b1d.min.js",
|
||||
"pdf.min.js": "pdf.4d9cc6ff49.min.js",
|
||||
"notebook.min.js": "notebook.c3144c18ae.min.js",
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unsLine truncated
|
||||
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
+6058
-4641
File diff suppressed because it is too large.
Load diff
@@ -15,3 +15,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<partial-view name="partials/footer.htm" :state="viewState"></partial-view>
|
||||
@@ -11,14 +11,16 @@
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary" :class="{ 'is-empty': total === 0 }">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }} <small>{{ total === 1 ? 'conference' : 'conferences' }}</small></span>
|
||||
<span class="summary-show-label">Show:</span>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
<div class="seg-bar">
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter by status">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'ready', 'is-empty': !segCount('ready') }" :aria-checked="segView() === 'ready'" @click="setSegView('ready')"><span class="seg-dot ok" aria-hidden="true"></span>Ready <span class="count">{{ fmt?.number(segCount('ready')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'preparing', 'is-empty': !segCount('preparing') }" :aria-checked="segView() === 'preparing'" @click="setSegView('preparing')"><span class="seg-dot warn" aria-hidden="true"></span>Preparing <span class="count">{{ fmt?.number(segCount('preparing')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'error', 'is-empty': !segCount('error') }" :aria-checked="segView() === 'error'" @click="setSegView('error')"><span class="seg-dot bad" aria-hidden="true"></span>Error <span class="count">{{ fmt?.number(segCount('error')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'expired', 'is-empty': !segCount('expired') }" :aria-checked="segView() === 'expired'" @click="setSegView('expired')"><span class="seg-dot off" aria-hidden="true"></span>Expired <span class="count">{{ fmt?.number(segCount('expired')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'removed', 'is-empty': !segCount('removed') }" :aria-checked="segView() === 'removed'" @click="setSegView('removed')"><span class="seg-dot off" aria-hidden="true"></span>Removed <span class="count">{{ fmt?.number(segCount('removed')) }}</span></button>
|
||||
</div>
|
||||
<p class="seg-countline" role="status"><strong>{{ total >= 0 ? fmt.number(total) : '…' }}</strong> shown</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
@@ -65,13 +67,13 @@
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
<span>{{ conference?.conferenceID }}</span> · <span>{{ fmt?.number(conference?.price || 0) }} €</span>
|
||||
<span>{{ conference?.conferenceID }}</span><span>{{ fmt?.money(conference?.price || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" :class="{'status-removed': conference?.status == 'removed' || conference?.status == 'expired', 'status-ready': conference?.status == 'ready', 'status-error': conference?.status == 'error', 'status-preparing': conference?.status == 'preparing'}"></span>
|
||||
<span v-text="fmt?.title(conference?.status)"></span>
|
||||
<span class="status-dot" :class="'st-' + (conference?.status || 'unknown')"></span>
|
||||
<span v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<a title="Show repositories in this conference" v-text="fmt?.number(conference?.repositories?.length || 0)" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"></a>
|
||||
@@ -99,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<span aria-hidden="true"></span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
|
||||
@@ -27,20 +27,20 @@
|
||||
<span v-if="!stats?.prev24h">no prior baseline</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-error kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'error' }" :aria-pressed="query?.bucket === 'error'" :title="'Show only 5xx errors'" @click="setBucket(query?.bucket === 'error' ? '' : 'error')" @keydown.enter.prevent="setBucket(query?.bucket === 'error' ? '' : 'error')">
|
||||
<div class="kpi-label">Errors (5xx)</div>
|
||||
<div class="kpi-card kpi-error kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'error' }" :aria-pressed="query?.bucket === 'error'" :title="'Show only errors (5xx)'" @click="setBucket(query?.bucket === 'error' ? '' : 'error')" @keydown.enter.prevent="setBucket(query?.bucket === 'error' ? '' : 'error')">
|
||||
<div class="kpi-label">Error</div>
|
||||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.error ?? 0) }}</div>
|
||||
<div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
|
||||
<div class="kpi-sub">5xx · {{ stats?.unique?.error }} unique</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-warn kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'warn' }" :aria-pressed="query?.bucket === 'warn'" :title="'Show only 4xx responses'" @click="setBucket(query?.bucket === 'warn' ? '' : 'warn')" @keydown.enter.prevent="setBucket(query?.bucket === 'warn' ? '' : 'warn')">
|
||||
<div class="kpi-label">4xx responses</div>
|
||||
<div class="kpi-card kpi-warn kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'warn' }" :aria-pressed="query?.bucket === 'warn'" :title="'Show only warnings (4xx, aborted requests)'" @click="setBucket(query?.bucket === 'warn' ? '' : 'warn')" @keydown.enter.prevent="setBucket(query?.bucket === 'warn' ? '' : 'warn')">
|
||||
<div class="kpi-label">Warning</div>
|
||||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.warn ?? 0) }}</div>
|
||||
<div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
|
||||
<div class="kpi-sub">4xx, aborted · {{ stats?.unique?.warn }} unique</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-info kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'info' }" :aria-pressed="query?.bucket === 'info'" :title="'Show only info events'" @click="setBucket(query?.bucket === 'info' ? '' : 'info')" @keydown.enter.prevent="setBucket(query?.bucket === 'info' ? '' : 'info')">
|
||||
<div class="kpi-label">Info (auth, 404)</div>
|
||||
<div class="kpi-label">Info</div>
|
||||
<div class="kpi-value">{{ fmt?.number(stats?.severity?.info ?? 0) }}</div>
|
||||
<div class="kpi-sub">{{ stats?.unique?.info }} unique</div>
|
||||
<div class="kpi-sub">Auth, 404 · {{ stats?.unique?.info }} unique</div>
|
||||
</div>
|
||||
<div class="kpi-card" :class="{'kpi-error': stats?.dropped > 0}">
|
||||
<div class="kpi-label">Distinct errors stored</div>
|
||||
@@ -56,9 +56,9 @@
|
||||
<div class="volume-head">
|
||||
<span class="volume-title">Volume · 24h · 1h buckets</span>
|
||||
<span class="volume-legend">
|
||||
<span v-if="stats?.severity?.error"><span class="dot dot-error"></span>error</span>
|
||||
<span v-if="stats?.severity?.warn"><span class="dot dot-warn"></span>warn</span>
|
||||
<span v-if="stats?.severity?.info"><span class="dot dot-info"></span>info</span>
|
||||
<span v-if="stats?.severity?.error"><span class="dot dot-error"></span>Error</span>
|
||||
<span v-if="stats?.severity?.warn"><span class="dot dot-warn"></span>Warning</span>
|
||||
<span v-if="stats?.severity?.info"><span class="dot dot-info"></span>Info</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="volume-bars">
|
||||
@@ -72,15 +72,15 @@
|
||||
</section>
|
||||
|
||||
<form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
|
||||
<div class="seg-tabs">
|
||||
<button type="button" :class="{active: query?.bucket === ''}" @click="setBucket('')">All</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'error'}" @click="setBucket('error')">5xx</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'warn'}" @click="setBucket('warn')">4xx</button>
|
||||
<button type="button" :class="{active: query?.bucket === 'info'}" @click="setBucket('info')">Info</button>
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter by severity">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === '' }" :aria-checked="(query?.bucket || '') === ''" @click="setBucket('')">All <span class="count">{{ fmt?.number((stats?.severity?.error ?? 0) + (stats?.severity?.warn ?? 0) + (stats?.severity?.info ?? 0)) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'error', 'is-empty': !(stats?.severity?.error) }" :aria-checked="(query?.bucket || '') === 'error'" @click="setBucket('error')"><span class="seg-dot bad" aria-hidden="true"></span>Error <span class="count">{{ fmt?.number(stats?.severity?.error ?? 0) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'warn', 'is-empty': !(stats?.severity?.warn) }" :aria-checked="(query?.bucket || '') === 'warn'" @click="setBucket('warn')"><span class="seg-dot warn" aria-hidden="true"></span>Warning <span class="count">{{ fmt?.number(stats?.severity?.warn ?? 0) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: (query?.bucket || '') === 'info', 'is-empty': !(stats?.severity?.info) }" :aria-checked="(query?.bucket || '') === 'info'" @click="setBucket('info')"><span class="seg-dot off" aria-hidden="true"></span>Info <span class="count">{{ fmt?.number(stats?.severity?.info ?? 0) }}</span></button>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<svg class="ui-icon search-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
|
||||
<input type="search" class="form-control" placeholder="Search code, module, status…" title="Syntax: code:repo_not_found module:route status:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<input type="search" class="form-control" placeholder="Search errors…" title="Search messages, or filter with code:repo_not_found module:route status:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
<div class="select-wrap">
|
||||
@@ -91,11 +91,11 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="select-wrap">
|
||||
<label>Group</label>
|
||||
<label title="Rows are always sectioned by severity. This merges repeated events inside each section.">Merge</label>
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.group = value }, value: query?.group, form: null, options: {} }">
|
||||
<option value="">Off</option>
|
||||
<option value="code">By code</option>
|
||||
<option value="module">By module</option>
|
||||
<option value="">Don't merge</option>
|
||||
<option value="code">Same error</option>
|
||||
<option value="module">Same module</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="autoref">
|
||||
@@ -121,7 +121,9 @@
|
||||
<span class="col-status">Status</span>
|
||||
</div>
|
||||
|
||||
<div class="errors-row" v-for="(row, index) in visible" :key="row?._key" :class="{open: expanded[row?._key]}">
|
||||
<template v-for="(row, index) in visible" :key="row?._key">
|
||||
<div class="errors-group-head" v-if="index === 0 || visible[index - 1]?._bucket !== row?._bucket"><span class="sev-dot" :class="'sev-' + row?._bucket"></span>{{ row?._bucket === 'error' ? 'Error' : row?._bucket === 'warn' ? 'Warning' : 'Info' }}<span class="errors-group-count">{{ fmt?.number(visible.filter(r => r?._bucket === row?._bucket).length) }}</span><span class="errors-group-note" v-if="index === 0">By severity · {{ query?.group === 'code' ? 'repeats merged by error · ' : query?.group === 'module' ? 'repeats merged by module · ' : '' }}{{ query?.sort === 'count' ? 'most frequent first' : 'most recent first' }}</span></div>
|
||||
<div class="errors-row" :class="{open: expanded[row?._key]}">
|
||||
<div class="errors-row-main" @click="toggle(row)">
|
||||
<div class="col-when">
|
||||
<div class="when-rel">{{ relTime(row?.ts) }}</div>
|
||||
@@ -129,14 +131,14 @@
|
||||
</div>
|
||||
<div class="col-sev">
|
||||
<span class="sev-dot" :class="'sev-' + row?._bucket"></span>
|
||||
<span class="sev-label">{{ fmt?.title(row?._bucket) }}</span>
|
||||
<span class="sev-label">{{ row?._bucket === 'error' ? 'Error' : row?._bucket === 'warn' ? 'Warning' : 'Info' }}</span>
|
||||
</div>
|
||||
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
|
||||
<div class="col-msg">
|
||||
<strong class="msg-code">{{ row?.displayMessage }}</strong>
|
||||
<span class="msg-context" v-if="row?.displayContext && row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
|
||||
<span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
|
||||
<div class="msg-url" v-if="row?._url">{{ row?._url }}</div>
|
||||
<div class="msg-url" v-if="row?._url" :title="row?._url">{{ row?._url }}</div>
|
||||
</div>
|
||||
<div class="col-count">
|
||||
<span class="count-pill" v-if="row?.count > 1">×{{ row?.count }}</span>
|
||||
@@ -144,6 +146,7 @@
|
||||
</div>
|
||||
<div class="col-status">
|
||||
<span class="status-pill" v-if="row?._status" :class="String(row?._status).match(/^[23]/) ? 'status-neutral' : ('status-' + row?._bucket)" :title="String(row?._status).match(/^2/) && row?._bucket === 'warn' ? 'Client closed the connection' : null">{{ String(row?._status).match(/^2/) && row?._bucket === 'warn' ? '—' : row?._status }}</span>
|
||||
<span class="status-empty" v-if="!row?._status" title="No HTTP status for this event">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,5 +198,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +1,12 @@
|
||||
<div class="container paper-page admin-page overview-page">
|
||||
<header class="page-head">
|
||||
<div class="paper-crumbs"><a href="/admin/">Admin</a> / <span class="here">Overview</span></div>
|
||||
|
||||
<div class="ov-header">
|
||||
<div class="ov-header page-head-main">
|
||||
<h1 class="paper-page-title">Overview</h1>
|
||||
<span class="ov-live"><span class="ov-live-dot" aria-hidden="true"></span>Live<span class="ov-live-extra"> · refreshes every 30s</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
@@ -27,7 +29,7 @@
|
||||
|
||||
<div class="ov-health" role="status" v-if="data?.system?.memPercent > 90 || data?.system?.diskPercent > 90">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
<span><b>Server needs attention.</b> Memory or disk is running high (see the cards below). New anonymizations may fail until it drops.</span>
|
||||
<span><b>Server needs attention.</b> {{ healthAlert() }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Top KPI row ──────────────────────────────────────────── -->
|
||||
@@ -39,18 +41,18 @@
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">CPU</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.cpuPercent > 80}">{{ data?.system?.cpuPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.cpuPercent > 80, crit: data?.system?.cpuPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.cpuPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores · load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.cpuPercent >= 75}">{{ data?.system?.cpuPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.cpuPercent >= 75, crit: data?.system?.cpuPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.cpuPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.cpuPercent > 90 }" v-if="data?.system?.cpuPercent >= 75">{{ data?.system?.cpuPercent > 90 ? 'Critical' : 'High' }} · </b>{{ data?.system?.cpuCount }} cores · load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Memory</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.memPercent > 85}">{{ data?.system?.memPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.memPercent > 85, crit: data?.system?.memPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.memPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.memPercent > 90 }" v-if="data?.system?.memPercent > 85">{{ data?.system?.memPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.memUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.memTotal) }}</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.memPercent >= 75}">{{ data?.system?.memPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.memPercent >= 75, crit: data?.system?.memPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.memPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.memPercent > 90 }" v-if="data?.system?.memPercent >= 75">{{ data?.system?.memPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.memUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.memTotal) }}</div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Disk</div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.diskPercent > 85}">{{ data?.system?.diskPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.diskPercent > 85, crit: data?.system?.diskPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.diskPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.diskPercent > 90 }" v-if="data?.system?.diskPercent > 85">{{ data?.system?.diskPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.diskUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.diskTotal) }}<template v-if="data?.system?.diskMount && data.system.diskMount !== '/'"> · {{ data?.system?.diskMount }}</template></div>
|
||||
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.diskPercent >= 75}">{{ data?.system?.diskPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.diskPercent >= 75, crit: data?.system?.diskPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.diskPercent || 0) + '%' }"></span></div>
|
||||
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.diskPercent > 90 }" v-if="data?.system?.diskPercent >= 75">{{ data?.system?.diskPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.diskUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.diskTotal) }}<template v-if="data?.system?.diskMount && data.system.diskMount !== '/'"> · {{ data?.system?.diskMount }}</template></div>
|
||||
</div>
|
||||
<div class="ov-kpi-card">
|
||||
<div class="ov-kpi-label">Uptime</div>
|
||||
@@ -69,7 +71,7 @@
|
||||
<div class="ov-daily-card">
|
||||
<div class="ov-daily-label">New users today</div>
|
||||
<div class="ov-daily-value">{{ fmt?.number(data?.daily?.today?.users || 0) }}</div>
|
||||
<div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total users</div>
|
||||
<div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total · +{{ fmt?.number(usersGained()) }} in 30 days</div>
|
||||
</div>
|
||||
<div class="ov-daily-card">
|
||||
<div class="ov-daily-label">Page views today</div>
|
||||
@@ -79,14 +81,15 @@
|
||||
</section>
|
||||
|
||||
<!-- ── Daily charts ─────────────────────────────────────────── -->
|
||||
<section class="ov-chart-row">
|
||||
<section class="ov-chart-row" :class="{ 'is-three': usersTiny() }">
|
||||
<div class="ov-chart-card">
|
||||
<div class="ov-chart-head">
|
||||
<span class="ov-chart-title">Daily page views · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day · peak {{ fmt?.number(historyMax('dailyPageViews')) }}</span>
|
||||
<span class="ov-chart-legends"><span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day · peak {{ fmt?.number(historyMax('dailyPageViews')) }}</span><span class="ov-legend-missing" v-if="historyHasMissing()"><span class="ov-swatch-missing" aria-hidden="true"></span>No data</span></span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"">
|
||||
<div class="ov-spark-bars ov-has-axis" v-if="data?.history?.length">
|
||||
<div class="ov-yaxis" aria-hidden="true"><span>{{ fmt?.number(axisMax('dailyPageViews')) }}</span><span>{{ fmt?.number(axisMax('dailyPageViews') / 2) }}</span><span>0</span></div>
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :class="{ 'is-missing': d?._empty }" :data-tip="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + (d?.dailyPageViews === 1 ? " view" : " views")" :aria-label="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + (d?.dailyPageViews === 1 ? " view" : " views")">
|
||||
<span class="ov-spark-fill" :style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,10 +102,11 @@
|
||||
<div class="ov-chart-card">
|
||||
<div class="ov-chart-head">
|
||||
<span class="ov-chart-title">New repos · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day · peak {{ fmt?.number(historyMax('dailyRepositories')) }}</span>
|
||||
<span class="ov-chart-legends"><span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day · peak {{ fmt?.number(historyMax('dailyRepositories')) }}</span><span class="ov-legend-missing" v-if="historyHasMissing()"><span class="ov-swatch-missing" aria-hidden="true"></span>No data</span></span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"">
|
||||
<div class="ov-spark-bars ov-has-axis" v-if="data?.history?.length">
|
||||
<div class="ov-yaxis" aria-hidden="true"><span>{{ fmt?.number(axisMax('dailyRepositories')) }}</span><span>{{ fmt?.number(axisMax('dailyRepositories') / 2) }}</span><span>0</span></div>
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :class="{ 'is-missing': d?._empty }" :data-tip="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + (d?.dailyRepositories === 1 ? " repo" : " repos")" :aria-label="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + (d?.dailyRepositories === 1 ? " repo" : " repos")">
|
||||
<span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,15 +116,18 @@
|
||||
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-chart-card">
|
||||
<div class="ov-chart-card" v-if="!usersTiny()">
|
||||
<div class="ov-chart-head">
|
||||
<span class="ov-chart-title">Users · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users · {{ fmt?.number(historyRange('nbUsers').min) }}–{{ fmt?.number(historyRange('nbUsers').max) }}</span>
|
||||
<span class="ov-chart-legends"><span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users · {{ fmt?.number(historyRange('nbUsers').max) }} now · +{{ fmt?.number(Math.max(0, historyRange('nbUsers').max - usersFirst())) }} in 30 days</span><span class="ov-legend-missing" v-if="historyHasMissing()"><span class="ov-swatch-missing" aria-hidden="true"></span>No data</span></span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"" :aria-label="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"">
|
||||
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyRangeH(d, 'nbUsers') + 'px'}"></span>
|
||||
</div>
|
||||
<div class="ov-spark-bars ov-spark-step ov-has-axis" v-if="data?.history?.length">
|
||||
<div class="ov-yaxis" aria-hidden="true"><span>{{ fmt?.number(userAxis().hi) }}</span><span>{{ fmt?.number((userAxis().hi + userAxis().lo) / 2) }}</span><span>{{ fmt?.number(userAxis().lo) }}</span></div>
|
||||
<svg class="ov-step-svg" :viewBox="'0 0 ' + data.history.length + ' 140'" preserveAspectRatio="none" role="img" :aria-label="'Total users over 30 days, from ' + fmt?.number(usersFirst()) + ' to ' + fmt?.number(historyRange('nbUsers').max)">
|
||||
<path class="ov-step-area" v-if="userAxis().lo === 0" :d="userStepPath(true)"></path>
|
||||
<path class="ov-step-line" :d="userStepPath(false)" vector-effect="non-scaling-stroke"></path>
|
||||
</svg>
|
||||
<div class="ov-step-missing" v-if="firstDataIndex() > 0" :style="{width: (firstDataIndex() / data.history.length * 100) + '%'}"></div>
|
||||
</div>
|
||||
<div class="ov-spark-x" v-if="data?.history?.length">
|
||||
<span>{{ historyLabel(data?.history[0]) }}</span>
|
||||
@@ -131,10 +138,11 @@
|
||||
<div class="ov-chart-card">
|
||||
<div class="ov-chart-head">
|
||||
<span class="ov-chart-title">New users · 30d</span>
|
||||
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day · peak {{ fmt?.number(historyMax('dailyUsers')) }}</span>
|
||||
<span class="ov-chart-legends"><span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day · peak {{ fmt?.number(historyMax('dailyUsers')) }}</span><span class="ov-legend-missing" v-if="historyHasMissing()"><span class="ov-swatch-missing" aria-hidden="true"></span>No data</span></span>
|
||||
</div>
|
||||
<div class="ov-spark-bars" v-if="data?.history?.length">
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"">
|
||||
<div class="ov-spark-bars ov-has-axis" v-if="data?.history?.length">
|
||||
<div class="ov-yaxis" aria-hidden="true"><span>{{ fmt?.number(axisMax('dailyUsers')) }}</span><span>{{ fmt?.number(axisMax('dailyUsers') / 2) }}</span><span>0</span></div>
|
||||
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :class="{ 'is-missing': d?._empty }" :data-tip="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + (d?.dailyUsers === 1 ? " user" : " users")" :aria-label="d?._empty ? historyLabel(d) + ': no data' : (historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + (d?.dailyUsers === 1 ? " user" : " users")">
|
||||
<span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,18 +164,18 @@
|
||||
<span class="ov-panel-meta">{{ fmt?.number(data?.repos?.total) }} total</span>
|
||||
</div>
|
||||
<div class="ov-stacked-bar">
|
||||
<span class="ov-bar-seg ov-bar-ready" title="Ready" :style="{width: barPct('ready') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct('preparing') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct('error') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct('expired') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct('removed') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-ready" v-if="barPct('ready') > 0" title="Ready" :style="{width: barPct('ready') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-preparing" v-if="barPct('preparing') > 0" title="Preparing" :style="{width: barPct('preparing') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-error" v-if="barPct('error') > 0" title="Error" :style="{width: barPct('error') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-expired" v-if="barPct('expired') > 0" title="Expiring / expired" :style="{width: barPct('expired') + '%'}"></span>
|
||||
<span class="ov-bar-seg ov-bar-removed" v-if="barPct('removed') > 0" title="Removing / removed" :style="{width: barPct('removed') + '%'}"></span>
|
||||
</div>
|
||||
<div class="ov-bar-legend">
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> Ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> Preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> Error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> Expiring / expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
|
||||
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> Removing / removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -175,24 +183,25 @@
|
||||
<div class="ov-panel-card">
|
||||
<div class="ov-panel-head">
|
||||
<span class="ov-panel-title">Errors · 24h</span>
|
||||
<span class="ov-panel-meta">{{ data?.errors?.last24h }} total</span>
|
||||
<span class="ov-panel-meta">{{ fmt?.number(realErrors()) }} {{ realErrors() === 1 ? 'error' : 'errors' }}</span>
|
||||
</div>
|
||||
<div class="ov-error-bars">
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">5xx</span>
|
||||
<span class="ov-ebar-label">Error</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" :style="{width: errPct('error') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.error }}</span>
|
||||
</div>
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">4xx</span>
|
||||
<span class="ov-ebar-label">Warning</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" :style="{width: errPct('warn') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.warn }}</span>
|
||||
</div>
|
||||
<div class="ov-ebar-row">
|
||||
<span class="ov-ebar-label">Info</span>
|
||||
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" :style="{width: errPct('info') + '%'}"></span></span>
|
||||
<span class="ov-ebar-n">{{ data?.errors?.severity?.info }}</span>
|
||||
<div class="ov-ehours" v-if="errorHours().length" role="img" :aria-label="'Errors per hour over the last 24 hours'">
|
||||
<div class="ov-ehours-legend"><span class="ov-ehours-key"><span class="ov-ehours-swatch is-error" aria-hidden="true"></span>Error</span><span class="ov-ehours-key"><span class="ov-ehours-swatch is-warn" aria-hidden="true"></span>Warning</span><span class="ov-ehours-note">Per hour<span class="seg-sep" aria-hidden="true">·</span>peak {{ fmt?.number(errorHoursPeak()) }}</span></div>
|
||||
<div class="ov-ehours-bars"><span v-for="h in errorHours()" :key="h.key" :class="{ 'is-bad': h.bad, 'is-zero': h.zero }" :title="h.title"><i class="ov-ehours-zero" v-if="h.zero"></i><template v-else><i class="ov-ehours-e" v-if="h.errPct" :style="{ height: h.errPct + '%' }"></i><i class="ov-ehours-w" v-if="h.warnPct" :style="{ height: h.warnPct + '%' }"></i></template></span></div>
|
||||
<div class="ov-ehours-x"><span>24h ago</span><span>now</span></div>
|
||||
</div>
|
||||
<p class="ov-ebar-note">Info {{ fmt?.number(data?.errors?.severity?.info || 0) }} · logged, not counted as errors</p>
|
||||
</div>
|
||||
<div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
|
||||
<a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state →</a>
|
||||
@@ -213,19 +222,19 @@
|
||||
<span class="ov-route-n ov-route-lat">Failed</span>
|
||||
</div>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">download</span>
|
||||
<span class="ov-route-name">Download</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.download?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.download?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.download?.failed > 0}">{{ data?.queues?.download?.failed }}</span>
|
||||
</a>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">remove</span>
|
||||
<span class="ov-route-name">Remove</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.remove?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.remove?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.remove?.failed > 0}">{{ data?.queues?.remove?.failed }}</span>
|
||||
</a>
|
||||
<a class="ov-route-row" href="/admin/queues">
|
||||
<span class="ov-route-name">cache</span>
|
||||
<span class="ov-route-name">Cache cleanup</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.cache?.active }}</span>
|
||||
<span class="ov-route-n">{{ data?.queues?.cache?.waiting }}</span>
|
||||
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.cache?.failed > 0}">{{ data?.queues?.cache?.failed }}</span>
|
||||
@@ -239,22 +248,21 @@
|
||||
<section class="ov-services-card">
|
||||
<div class="ov-services-head">
|
||||
<span class="ov-panel-title">Services</span>
|
||||
<span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users · {{ data?.conferences?.total }} conferences</span>
|
||||
<span class="ov-panel-meta">5 services · {{ realErrors() > 0 ? '1 needs attention' : 'all healthy' }}</span>
|
||||
</div>
|
||||
<div class="ov-services-grid">
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot ov-dot-ok"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">web</span>
|
||||
<span class="ov-svc-meta">{{ data?.system?.nodeVersion }}</span>
|
||||
<span class="ov-svc-meta">{{ data?.system?.nodeVersion }} · up {{ humanDuration(data?.system?.uptime) }}</span>
|
||||
</div>
|
||||
<span class="ov-svc-detail">uptime {{ humanDuration(data?.system?.uptime) }}</span>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">download</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} {{ queueTotal(data?.queues?.download) === 1 ? 'job' : 'jobs' }}</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.download)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,7 +270,7 @@
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">cache</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} {{ queueTotal(data?.queues?.cache) === 1 ? 'job' : 'jobs' }}</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.cache)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,15 +278,15 @@
|
||||
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">remove</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} jobs</span>
|
||||
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} {{ queueTotal(data?.queues?.remove) === 1 ? 'job' : 'jobs' }}</span>
|
||||
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.remove)">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-svc">
|
||||
<span class="ov-svc-dot" :class="data?.repos?.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span>
|
||||
<span class="ov-svc-dot" :class="realErrors() > 0 ? 'ov-dot-warn' : 'ov-dot-ok'" :title="realErrors() > 0 ? 'Errors in the last 24 hours' : 'No errors in the last 24 hours'"></span>
|
||||
<div class="ov-svc-info">
|
||||
<span class="ov-svc-name">errors</span>
|
||||
<span class="ov-svc-meta">{{ data?.errors?.last24h }} / 24h</span>
|
||||
<span class="ov-svc-meta">{{ fmt?.number(realErrors()) }} / 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,7 +297,7 @@
|
||||
<section class="ov-panel-card ov-cache" aria-labelledby="cache-heading" v-show="ovView === 'cache'">
|
||||
<div class="ov-panel-head ov-performance-head">
|
||||
<div><h2 class="ov-panel-title" id="cache-heading">Cache</h2>
|
||||
<p class="ov-performance-note">Shared storage, repeated reads and anonymization work · refreshes every 30 seconds</p></div>
|
||||
<p class="ov-performance-note">Shared storage, repeated reads and anonymization work</p></div>
|
||||
<div class="ov-performance-window">
|
||||
<label for="cache-window">Time range</label>
|
||||
<select id="cache-window" class="form-control form-control-sm" v-model.number="cacheMinutes" @change="loadCache()">
|
||||
@@ -314,22 +322,26 @@
|
||||
<div class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Cache storage budgets" v-if="cacheReport.source">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Current shared-cache storage and byte budgets</caption>
|
||||
<thead><tr><th scope="col">Pool</th><th scope="col">Allocated / budget</th><th scope="col">Reserved for fills</th><th scope="col">Original bytes</th><th scope="col">Blobs</th><th scope="col">Active readers</th></tr></thead>
|
||||
<thead><tr><th scope="col">Pool</th><th scope="col" class="num">Allocated / budget</th><th scope="col" class="num">Reserved for fills</th><th scope="col" class="num">Original bytes</th><th scope="col" class="num">Blobs</th><th scope="col" class="num">Active readers</th></tr></thead>
|
||||
<tbody><tr v-for="pool in [{name: 'Source blobs', data: cacheReport.source}, {name: 'Transformed output', data: cacheReport.results}]" :key="pool.name" v-show="pool.data">
|
||||
<th scope="row">{{ pool.name }}</th><td>{{ humanBytes(pool.data?.allocatedBytes) }} / {{ humanBytes(pool.data?.budgetBytes) }}</td>
|
||||
<td>{{ humanBytes(pool.data?.reservedBytes) }}</td><td>{{ humanBytes(pool.data?.originalBytes) }}</td>
|
||||
<td>{{ pool.data?.blobs }}</td><td>{{ pool.data?.activeReaders }}</td></tr></tbody>
|
||||
<th scope="row" class="ov-event-name">{{ pool.name }}</th><td class="num budget-cell" :class="{ 'is-over': pool.data?.budgetBytes && pool.data?.allocatedBytes > pool.data?.budgetBytes }" :title="pool.data?.budgetBytes ? Math.round(100 * (pool.data?.allocatedBytes || 0) / pool.data.budgetBytes) + '% of budget' : null"><span class="ov-metric-bad-text" v-if="pool.data?.budgetBytes && pool.data?.allocatedBytes > pool.data?.budgetBytes">Over budget · </span>{{ humanBytes(pool.data?.allocatedBytes) }} / {{ humanBytes(pool.data?.budgetBytes) }}<span class="budget-bar" v-if="pool.data?.budgetBytes" aria-hidden="true"><i :style="{ width: Math.min(100, Math.round(100 * (pool.data?.allocatedBytes || 0) / pool.data.budgetBytes)) + '%' }"></i></span></td>
|
||||
<td class="num">{{ humanBytes(pool.data?.reservedBytes) }}</td><td class="num">{{ humanBytes(pool.data?.originalBytes) }}</td>
|
||||
<td class="num">{{ fmt?.number(pool.data?.blobs) }}</td><td class="num">{{ fmt?.number(pool.data?.activeReaders) }}</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="ov-performance-note" v-if="cacheReport.source">Source request hit rate: {{ cacheRate('source', 'count') }} · Source byte hit rate: {{ cacheRate('source', 'bytes') }} · Decision hit rate: {{ cacheRate('decision', 'count') }}</p>
|
||||
<dl class="ov-cache-rates" v-if="cacheReport.source">
|
||||
<div><dt>Source request hit rate</dt><dd>{{ cacheRate('source', 'count') }}</dd></div>
|
||||
<div><dt>Source byte hit rate</dt><dd>{{ cacheRate('source', 'bytes') }}</dd></div>
|
||||
<div><dt>Decision hit rate</dt><dd>{{ cacheRate('decision', 'count') }}</dd></div>
|
||||
</dl>
|
||||
<p class="ov-performance-note">Reuse counts measure consecutive reads of the same blob within each interval. Codec timings are elapsed stream time, not CPU time; eviction times measure how long a blob was idle. Known monitors are identified by request metadata; other automation may remain in file reads.</p>
|
||||
<div v-if="cacheRows().length" class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Cache events and processing durations">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Cache events for the selected time range and traffic</caption>
|
||||
<thead><tr><th scope="col">Event</th><th scope="col">Traffic</th><th scope="col">Count</th><th scope="col">Bytes</th><th scope="col">Mean measured time</th><th scope="col">p95 bound</th></tr></thead>
|
||||
<thead><tr><th scope="col">Event</th><th scope="col">Traffic</th><th scope="col" class="num">Count</th><th scope="col" class="num">Bytes</th><th scope="col" class="num">Mean measured time</th><th scope="col" class="num">p95 bound</th></tr></thead>
|
||||
<tbody><tr v-for="row in cacheRows()" :key="row.name + ':' + row.traffic">
|
||||
<th scope="row">{{ cacheEventName(row.name) }}</th><td>{{ row.traffic }}</td><td>{{ row.count }}</td><td>{{ humanBytes(row.bytes) }}</td>
|
||||
<td>{{ row.ms ? (row.ms / row.count).toFixed(2) + " ms" : "Not timed" }}</td><td>{{ cacheP95(row) }}</td></tr></tbody>
|
||||
<th scope="row" class="ov-event-name">{{ cacheEventName(row.name) }}</th><td class="ov-event-traffic">{{ row.traffic }}</td><td class="num">{{ fmt?.number(row.count) }}</td><td class="num">{{ humanBytes(row.bytes) }}</td>
|
||||
<td class="num" :class="{ 'ov-muted': !row.ms }">{{ row.ms ? fmt.duration(row.ms / row.count) : "Not timed" }}</td><td class="num" :class="{ 'ov-muted': !row.ms }">{{ cacheP95(row) }}</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="ov-performance-empty" role="status">No cache events in this time range for the selected traffic.</p>
|
||||
@@ -341,9 +353,13 @@
|
||||
<div class="ov-panel-head ov-performance-head">
|
||||
<div>
|
||||
<h2 class="ov-panel-title" id="performance-heading">Request performance</h2>
|
||||
<p class="ov-performance-note">Request latency and process health · refreshes every 30 seconds</p>
|
||||
<p class="ov-performance-note">Request latency and process health</p>
|
||||
</div>
|
||||
<div class="ov-performance-window">
|
||||
<template v-if="performance?.available"><label for="performance-view">Show</label>
|
||||
<select id="performance-view" class="form-control form-control-sm" v-model="performanceTab" aria-label="Performance metrics">
|
||||
<option value="routes">Routes</option><option value="processes">Processes</option><option value="stages">Work stages</option>
|
||||
</select></template>
|
||||
<label for="performance-window">Time range</label>
|
||||
<select id="performance-window" class="form-control form-control-sm" v-model.number="performanceMinutes" @change="loadPerformance()" aria-label="Performance time range">
|
||||
<option :value="15">Last 15 minutes</option><option :value="60">Last hour</option>
|
||||
@@ -357,12 +373,7 @@
|
||||
</div>
|
||||
<p v-if="!performance && !performanceError" class="ov-performance-empty" role="status">Loading performance monitoring…</p>
|
||||
<div v-if="performance?.available">
|
||||
<div class="paper-tabs ov-performance-tabs" role="tablist" aria-label="Performance metrics">
|
||||
<button id="performance-routes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'routes'}" :aria-selected="performanceTab === 'routes'" :tabindex="performanceTab === 'routes' ? 0 : -1" aria-controls="performance-routes" @click="performanceTab = 'routes'" @keydown="previewTabKeydown($event)">Routes</button>
|
||||
<button id="performance-processes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'processes'}" :aria-selected="performanceTab === 'processes'" :tabindex="performanceTab === 'processes' ? 0 : -1" aria-controls="performance-processes" @click="performanceTab = 'processes'" @keydown="previewTabKeydown($event)">Processes</button>
|
||||
<button id="performance-stages-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'stages'}" :aria-selected="performanceTab === 'stages'" :tabindex="performanceTab === 'stages' ? 0 : -1" aria-controls="performance-stages" @click="performanceTab = 'stages'" @keydown="previewTabKeydown($event)">Work stages</button>
|
||||
</div>
|
||||
<div id="performance-routes" role="tabpanel" aria-labelledby="performance-routes-tab" v-show="performanceTab === 'routes'">
|
||||
<div id="performance-routes" role="region" aria-label="Routes" v-show="performanceTab === 'routes'">
|
||||
<div class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Route latency metrics, scroll for more columns" v-if="performance.routes?.length">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Route latency for the selected time range</caption>
|
||||
@@ -371,13 +382,13 @@
|
||||
<th scope="row" class="ov-metric-name"><span>{{ row.route }}</span><small>{{ row.service }} · {{ row.method }}</small></th>
|
||||
<td class="num">{{ fmt.number(row.count) }}</td><td class="num">{{ formatMs(row.avgMs) }}</td>
|
||||
<td class="num">{{ firstByteBound(row) }}</td><td class="num">{{ latencyBound(row, 'p95UpperMs') }}</td><td class="num">{{ latencyBound(row, 'p99UpperMs') }}</td>
|
||||
<td class="num" :class="{'ov-metric-warn': row.aborted > 0}">{{ fmt.number(row.aborted) }}</td><td class="num" :class="{'ov-metric-warn': row.errors > 0}">{{ fmt.number(row.errors) }}</td><td class="num" :class="{'ov-metric-warn': row.slow > 0}">{{ fmt.number(row.slow) }}</td>
|
||||
<td class="num" :class="{'ov-metric-warn': row.aborted > 0}">{{ fmt.number(row.aborted) }}</td><td class="num" :class="{'ov-metric-bad': row.errors > 0}">{{ fmt.number(row.errors) }}</td><td class="num" :class="{'ov-metric-warn': row.slow > 0}">{{ fmt.number(row.slow) }}</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="ov-performance-empty">No recorded requests in this time range yet.</p>
|
||||
</div>
|
||||
<div id="performance-processes" role="tabpanel" aria-labelledby="performance-processes-tab" v-show="performanceTab === 'processes'">
|
||||
<div id="performance-processes" role="region" aria-label="Processes" v-show="performanceTab === 'processes'">
|
||||
<p class="ov-performance-note">API and streamer processes · latest sample per instance</p>
|
||||
<div v-if="!performance.instances?.length" class="ov-performance-message ov-performance-warning" role="status">No recent process samples. Monitoring coverage is incomplete.</div>
|
||||
<div v-else class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Process resource metrics, scroll for more columns">
|
||||
@@ -387,7 +398,7 @@
|
||||
<tbody><tr v-for="sample in performance.instances" :key="sample.instance" :class="{'ov-metric-warning-row': performanceWarning(sample)}">
|
||||
<th scope="row" class="ov-metric-name"><span>{{ sample.instance }}</span><small class="ov-metric-warn" v-if="performanceWarning(sample)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> Needs attention</small></th>
|
||||
<td class="num">{{ humanBytes(sample.memory.rss) }} / {{ humanBytes(sample.memoryLimitBytes) }}<small>Peak {{ humanBytes(peakRss(sample)) }}</small></td>
|
||||
<td class="num">{{ sample.cpuPercent == null ? 'Unavailable' : sample.cpuPercent.toFixed(1) + '%' }}</td><td class="num">{{ sample.eventLoop?.p95Ms == null ? 'Not applicable' : sample.eventLoop.p95Ms.toFixed(1) + ' ms' }}</td>
|
||||
<td class="num">{{ sample.cpuPercent == null ? 'Unavailable' : sample.cpuPercent.toFixed(1) + '%' }}</td><td class="num">{{ sample.eventLoop?.p95Ms == null ? 'Not applicable' : fmt.duration(sample.eventLoop.p95Ms) }}</td>
|
||||
<td class="num">{{ sample.activeRequests }}</td><td class="num">{{ sample.sockets?.descriptors ?? 'Unavailable' }} / {{ sample.sockets?.closeWait ?? 'Unavailable' }}</td>
|
||||
<td class="num">{{ sample.workers?.running ?? 0 }} / {{ sample.workers?.waiting ?? 0 }}</td><td class="num">{{ sampleAge(sample) }} s</td>
|
||||
<td class="num">{{ sample.droppedBatches }} / {{ sample.droppedMetrics }}</td>
|
||||
@@ -395,7 +406,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="performance-stages" role="tabpanel" aria-labelledby="performance-stages-tab" v-show="performanceTab === 'stages'">
|
||||
<div id="performance-stages" role="region" aria-label="Work stages" v-show="performanceTab === 'stages'">
|
||||
<div v-if="performance.stages?.some(row => row.metric !== 'first_byte')" class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Work stage metrics, scroll for more columns">
|
||||
<table class="ov-metrics-table">
|
||||
<caption class="sr-only">Work stages and cache results</caption>
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
<div class="q-cards">
|
||||
<div class="q-card" role="button" tabindex="0" :aria-pressed="selectedQueue === q.key" @keydown.enter.prevent="selectQueue(q.key)" @keydown.space.prevent="selectQueue(q.key)" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
|
||||
<div class="q-card-head">
|
||||
<span class="q-dot" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0, 'q-dot-idle': !q?.counts?.active && !q?.counts?.waiting}"></span>
|
||||
<span class="q-dot" :title="q?.paused ? 'Paused' : q?.counts?.failed > 0 ? q.counts.failed + ' failed jobs' : (q?.counts?.active || q?.counts?.waiting) ? 'Running' : 'Idle'" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0, 'q-dot-idle': !q?.counts?.active && !q?.counts?.waiting}"></span>
|
||||
<span class="q-card-name" v-text="q?.label"></span>
|
||||
</div>
|
||||
<div class="q-card-count" v-text="(q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0)"></div>
|
||||
<div class="q-card-sub">
|
||||
<span>waiting · {{ q?.counts?.active || 0 }} active</span>
|
||||
<span>waiting · {{ q?.counts?.active || 0 }} active<template v-if="q?.counts?.failed"> · <b class="q-card-failed">{{ q.counts.failed }} failed</b></template><template v-if="q?.paused"> · <b class="q-card-failed">paused</b></template></span>
|
||||
<div class="q-card-bar" v-if="q?.counts?.active">
|
||||
<div class="q-card-bar-fill" :style="{width: (q?.counts?.active / ((q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0) || 1) * 100) + '%'}"></div>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<button class="btn btn-sm" :class="{active: range == '24h'}" @click="setRange('24h')">24h</button>
|
||||
<button class="btn btn-sm" :class="{active: range == '7d'}" @click="setRange('7d')">7d</button>
|
||||
</div>
|
||||
<div class="q-section-label">Throughput · {{ selectedQueue }} <span class="q-section-right" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <template v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-exec" aria-hidden="true"></span> avg time · {{ range }}</template></span></div>
|
||||
<div class="q-section-label">Throughput · {{ selectedQueue }} <span class="q-section-right" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-completed">●</span> Completed <span class="q-legend-failed">●</span> Failed <template v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-exec" aria-hidden="true"></span> Avg time · {{ range }}</template></span></div>
|
||||
<div class="q-chart-wrap">
|
||||
<canvas id="q-throughput-chart" height="180"></canvas>
|
||||
<div class="q-chart-empty" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) === 0"><strong>No jobs processed in this window</strong><span>Completed and failed jobs will chart here.</span></div>
|
||||
@@ -96,11 +96,13 @@
|
||||
<div class="q-jobs">
|
||||
<div class="q-jobs-header">
|
||||
<div class="q-section-label">All jobs · {{ selectedQueue }}</div>
|
||||
<div class="q-state-filters">
|
||||
<label class="q-state-toggle" v-for="(s, index) in allStates">
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
|
||||
<span :class=""q-state-chip q-state-" + (s)">{{ s }}</span>
|
||||
</label>
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter jobs by state">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'active', 'is-empty': !segCount('active') }" :aria-checked="segView() === 'active'" @click="setSegView('active')"><span class="seg-dot ok" aria-hidden="true"></span>Active <span class="count">{{ fmt?.number(segCount('active')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'waiting', 'is-empty': !segCount('waiting') }" :aria-checked="segView() === 'waiting'" @click="setSegView('waiting')"><span class="seg-dot warn" aria-hidden="true"></span>Waiting <span class="count">{{ fmt?.number(segCount('waiting')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'delayed', 'is-empty': !segCount('delayed') }" :aria-checked="segView() === 'delayed'" @click="setSegView('delayed')"><span class="seg-dot warn" aria-hidden="true"></span>Delayed <span class="count">{{ fmt?.number(segCount('delayed')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'failed', 'is-empty': !segCount('failed') }" :aria-checked="segView() === 'failed'" @click="setSegView('failed')"><span class="seg-dot bad" aria-hidden="true"></span>Failed <span class="count">{{ fmt?.number(segCount('failed')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'completed', 'is-empty': !segCount('completed') }" :aria-checked="segView() === 'completed'" @click="setSegView('completed')"><span class="seg-dot done" aria-hidden="true"></span>Completed <span class="count">{{ fmt?.number(segCount('completed')) }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,10 +119,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<th>Job ID</th>
|
||||
<th>Payload</th>
|
||||
<th>Job</th>
|
||||
<th>Detail</th>
|
||||
<th>Attempts</th>
|
||||
<th>Duration</th>
|
||||
<th class="q-th-num">Duration</th>
|
||||
<th>Progress</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -131,20 +133,17 @@
|
||||
<span v-text="job?._state" :class=""q-state-badge q-state-" + (job?._state)"></span>
|
||||
<span class="q-delay-hint" v-if="job?._state == 'delayed' && job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
|
||||
</td>
|
||||
<td class="q-cell-id">
|
||||
<td class="q-cell-id" :title="'job:' + job?.id + (job?.name && job?.name !== job?.id ? ' · ' + job.name : '') + (job?.data?.repoId && job?.data?.repoId !== job?.id ? ' · ' + job.data.repoId : '')">
|
||||
<svg class="ui-icon q-chevron" :class="{'q-chevron-open': expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
||||
<a target="_blank" v-text="'job:' + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.id))"></a>
|
||||
</td>
|
||||
<td class="q-cell-payload">
|
||||
<span v-text="job?.name || 'anonymize'"></span>
|
||||
<span class="q-payload-detail" v-if="job?.data?.repoId && job?.data?.repoId !== job?.name"> · {{ job?.data?.repoId }}</span>
|
||||
<a target="_blank" class="q-job-link" v-text="job?.data?.repoId || job?.id" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.data?.repoId || job?.id))"></a>
|
||||
</td>
|
||||
<td class="q-cell-detail" :class="{'is-error': job?._state == 'failed'}" :title="job?._state == 'failed' ? job?.failedReason : ''" v-text="jobDetail(job)"></td>
|
||||
<td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
|
||||
<td class="q-cell-num" v-text="jobDuration(job)"></td>
|
||||
<td class="q-cell-num q-cell-duration" v-text="jobDuration(job)"></td>
|
||||
<td class="q-cell-progress">
|
||||
<div class="q-progress-wrap" v-if="jobProgressPct(job) !== null">
|
||||
<div class="q-progress-bar" :style="{'--pct': jobProgressPct(job) + '%'}"></div>
|
||||
<span class="q-progress-label" v-text="jobProgressPct(job) + '%'"></span>
|
||||
<div class="q-progress-wrap" :class="{'is-indeterminate': jobProgressPct(job) === null}" :aria-label="jobProgressPct(job) === null ? 'Progress not reported' : jobProgressPct(job) + '% done'">
|
||||
<div class="q-progress-bar" :style="{'--pct': (jobProgressPct(job) || 0) + '%'}"></div>
|
||||
<span class="q-progress-label" v-text="jobProgressPct(job) === null ? '—' : jobProgressPct(job) + '%'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="q-cell-actions">
|
||||
|
||||
@@ -11,15 +11,16 @@
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }} <small>{{ total === 1 ? 'repository' : 'repositories' }}</small></span>
|
||||
<span class="summary-meta">{{ fmt?.humanFileSize(totalSize) }} on disk</span>
|
||||
<span class="summary-show-label">Show:</span>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing') + statusCountFor('download')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired') + statusCountFor('expiring')) }}</span></button>
|
||||
<button type="button" class="summary-pill" :title="query?.removed ? 'Removed repositories are shown. Click to hide them.' : 'Removed repositories are hidden. Click to include them.'" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed') + statusCountFor('removing')) }}</span></button>
|
||||
<div class="seg-bar">
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter by status">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'ready', 'is-empty': !segCount('ready') }" :aria-checked="segView() === 'ready'" @click="setSegView('ready')"><span class="seg-dot ok" aria-hidden="true"></span>Ready <span class="count">{{ fmt?.number(segCount('ready')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'preparing', 'is-empty': !segCount('preparing') }" :aria-checked="segView() === 'preparing'" @click="setSegView('preparing')"><span class="seg-dot warn" aria-hidden="true"></span>Preparing <span class="count">{{ fmt?.number(segCount('preparing')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'error', 'is-empty': !segCount('error') }" :aria-checked="segView() === 'error'" @click="setSegView('error')"><span class="seg-dot bad" aria-hidden="true"></span>Error <span class="count">{{ fmt?.number(segCount('error')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'expired', 'is-empty': !segCount('expired') }" :aria-checked="segView() === 'expired'" @click="setSegView('expired')"><span class="seg-dot off" aria-hidden="true"></span>Expiring / expired <span class="count">{{ fmt?.number(segCount('expired')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'removed', 'is-empty': !segCount('removed') }" :aria-checked="segView() === 'removed'" @click="setSegView('removed')"><span class="seg-dot off" aria-hidden="true"></span>Removing / removed <span class="count">{{ fmt?.number(segCount('removed')) }}</span></button>
|
||||
</div>
|
||||
<p class="seg-countline" role="status"><strong>{{ total >= 0 ? fmt.number(total) : '…' }}</strong> shown<span class="seg-sep">·</span><span class="seg-meta">{{ fmt?.humanFileSize(totalSize) }} on disk</span></p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
@@ -83,19 +84,19 @@
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span class="meta-sep" v-if="repo?.options?.update"><a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span class="meta-sep" v-if="!repo?.options?.update"><a class="mono-hash" v-text="repo?.source?.commit?.substring(0, 7)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span class="meta-sep" v-if="repo?.conference"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span class="meta-sep" v-if="repo?.size?.storage > 0">{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span>{{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span>
|
||||
<div class="anon-sub anon-meta" data-meta-fit :title="[repo?.source?.fullName || repo?.source?.repositoryName, repo?.options?.update ? 'branch ' + repo?.source?.branch : 'commit ' + repo?.source?.commit?.substring(0, 7), repo?.conference ? 'conference ' + repo?.conference : '', repo?.size?.storage > 0 ? fmt?.humanFileSize(repo?.size?.storage) : '', fmt?.number(repo?.options?.terms?.length) + (repo?.options?.terms?.length === 1 ? ' term' : ' terms')].filter(Boolean).join(' · ')">
|
||||
<a class="src-path" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"><span class="src-owner" v-if="(repo?.source?.repositoryName || '').includes('/')" v-text="(o => o.length > 16 ? o.slice(0, 14) + '…' : o)((repo?.source?.repositoryName || '').split('/')[0])"></span><span class="src-repo" v-text="(repo?.source?.repositoryName || '').includes('/') ? '/' + (repo?.source?.repositoryName || '').split('/').slice(1).join('/') : repo?.source?.repositoryName"></span></a><span class="meta-item" v-if="!repo?.options?.update" :data-meta-label="'commit ' + (repo?.source?.commit || '').substring(0, 7)"><a class="mono-hash" :title="repo?.source?.commit" v-text="repo?.source?.commit?.substring(0, 7)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span class="meta-item" v-if="repo?.options?.update" :data-meta-label="'branch ' + (repo?.source?.branch || '')"><a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span class="meta-item meta-conf" v-if="repo?.conference" :data-meta-label="'conference ' + repo?.conference"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span class="meta-item" v-if="repo?.size?.storage > 0">{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span class="meta-item">{{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span><span class="meta-more" hidden></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-line">
|
||||
<span class="status-dot" :class="{'status-removed': repo?.status == 'removed' || repo?.status == 'expired', 'status-ready': repo?.status == 'ready', 'status-error': repo?.status == 'error', 'status-preparing': repo?.status == 'preparing'}"></span>
|
||||
<span v-text="fmt?.title(repo?.status)"></span>
|
||||
<span class="status-dot" :class="'st-' + (repo?.status || 'unknown')"></span>
|
||||
<span v-text="fmt?.statusLabel(repo?.status)"></span>
|
||||
</span>
|
||||
<span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(repo?.statusMessage)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView || 0)"></span></div>
|
||||
<div class="cell-views num" role="cell" :data-unit="(repo?.pageView || 0) === 1 ? 'view' : 'views'"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView || 0)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="(fmt?.humanTime(repo?.anonymizeDate) || '').replace(/^on /, '')"></span></div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
@@ -126,7 +127,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<span aria-hidden="true"></span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
<div class="user-header">
|
||||
<img width="56" height="56" v-if="userInfo?.photo" :src="safeUrl((userInfo?.photo))">
|
||||
<div>
|
||||
<h2>
|
||||
{{ userInfo?.username }}
|
||||
<div class="user-header-status">
|
||||
<span class="status-dot-wrap">
|
||||
<span class="status-dot" :class="{'status-ready': userInfo?.status == 'active', 'status-removed': userInfo?.status != 'active'}"></span>
|
||||
<span class="status-dot" :class="'st-' + (userInfo?.status || 'unknown')"></span>
|
||||
<span v-text="fmt?.title(userInfo?.status)"></span>
|
||||
</span>
|
||||
<span class="type-badge type-repo" v-if="userInfo?.isAdmin">Admin</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="user-actions" style="margin-top: 4px;">
|
||||
<button class="btn btn-sm btn-outline-danger" v-if="userInfo?.status !== 'banned' && userInfo?.username !== user?.username" @click="banUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Activate</button>
|
||||
@@ -134,19 +133,22 @@
|
||||
<span class="section-count">{{ repositories?.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ fmt?.number(repositories?.length) }}</span>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: filters?.status?.ready === false}" @click="filters.status.ready = !filters.status.ready" :aria-pressed="!!(filters?.status?.ready === false)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: filters?.status?.preparing === false}" @click="filters.status.preparing = !filters.status.preparing" :aria-pressed="!!(filters?.status?.preparing === false)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: filters?.status?.error === false}" @click="filters.status.error = !filters.status.error" :aria-pressed="!!(filters?.status?.error === false)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: filters?.status?.expired === false}" @click="filters.status.expired = !filters.status.expired" :aria-pressed="!!(filters?.status?.expired === false)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: filters?.status?.removed === false}" @click="filters.status.removed = !filters.status.removed" :aria-pressed="!!(filters?.status?.removed === false)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
<div class="seg-bar">
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter by status">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'ready', 'is-empty': !segCount('ready') }" :aria-checked="segView() === 'ready'" @click="setSegView('ready')"><span class="seg-dot ok" aria-hidden="true"></span>Ready <span class="count">{{ fmt?.number(segCount('ready')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'preparing', 'is-empty': !segCount('preparing') }" :aria-checked="segView() === 'preparing'" @click="setSegView('preparing')"><span class="seg-dot warn" aria-hidden="true"></span>Preparing <span class="count">{{ fmt?.number(segCount('preparing')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'error', 'is-empty': !segCount('error') }" :aria-checked="segView() === 'error'" @click="setSegView('error')"><span class="seg-dot bad" aria-hidden="true"></span>Error <span class="count">{{ fmt?.number(segCount('error')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'expired', 'is-empty': !segCount('expired') }" :aria-checked="segView() === 'expired'" @click="setSegView('expired')"><span class="seg-dot off" aria-hidden="true"></span>Expiring / expired <span class="count">{{ fmt?.number(segCount('expired')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'removed', 'is-empty': !segCount('removed') }" :aria-checked="segView() === 'removed'" @click="setSegView('removed')"><span class="seg-dot off" aria-hidden="true"></span>Removing / removed <span class="count">{{ fmt?.number(segCount('removed')) }}</span></button>
|
||||
</div>
|
||||
<p class="seg-countline" role="status"><strong>{{ fmt.number(filteredRepositories?.length || 0) }}</strong> shown</p>
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
<div class="admin-filter-row">
|
||||
<div class="search-wrap">
|
||||
<input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
<input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repositories or errors…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
@@ -179,19 +181,19 @@
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span>
|
||||
<div class="anon-sub anon-meta" data-meta-fit :title="[repo?.source?.fullName || repo?.source?.repositoryName, repo?.options?.update ? 'branch ' + repo?.source?.branch : 'commit ' + repo?.source?.commit?.substring(0, 7), repo?.conference ? 'conference ' + repo?.conference : '', repo?.size?.storage > 0 ? fmt?.humanFileSize(repo?.size?.storage) : '', fmt?.number(repo?.options?.terms?.length) + (repo?.options?.terms?.length === 1 ? ' term' : ' terms')].filter(Boolean).join(' · ')">
|
||||
<a class="src-path" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"><span class="src-owner" v-if="(repo?.source?.fullName || '').includes('/')" v-text="(o => o.length > 16 ? o.slice(0, 14) + '…' : o)((repo?.source?.fullName || '').split('/')[0])"></span><span class="src-repo" v-text="(repo?.source?.fullName || '').includes('/') ? '/' + (repo?.source?.fullName || '').split('/').slice(1).join('/') : repo?.source?.fullName"></span></a><span class="meta-item" v-if="!repo?.options?.update" :data-meta-label="'commit ' + (repo?.source?.commit || '').substring(0, 7)"><a class="mono-hash" :title="repo?.source?.commit" v-text="repo?.source?.commit?.substring(0, 7)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span class="meta-item" v-if="repo?.options?.update" :data-meta-label="'branch ' + (repo?.source?.branch || '')"><a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span class="meta-item meta-conf" v-if="repo?.conference" :data-meta-label="'conference ' + repo?.conference"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span class="meta-item" v-if="repo?.size?.storage > 0">{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span class="meta-item">{{ fmt?.number(repo?.options?.terms?.length) }} {{ repo?.options?.terms?.length === 1 ? 'term' : 'terms' }}</span><span class="meta-more" hidden></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-line">
|
||||
<span class="status-dot" :class="{'status-removed': repo?.status == 'removed' || repo?.status == 'expired', 'status-ready': repo?.status == 'ready', 'status-error': repo?.status == 'error', 'status-preparing': repo?.status == 'preparing'}"></span>
|
||||
<span v-text="fmt?.title(repo?.status)"></span>
|
||||
<span class="status-dot" :class="'st-' + (repo?.status || 'unknown')"></span>
|
||||
<span v-text="fmt?.statusLabel(repo?.status)"></span>
|
||||
</span>
|
||||
<span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(repo?.statusMessage)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-views num" role="cell" :data-unit="(repo?.pageView || 0) === 1 ? 'view' : 'views'" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }} <small>{{ total === 1 ? 'user' : 'users' }}</small></span>
|
||||
<button type="button" class="summary-pill" :class="{active: !query?.status}" @click="query.status = ''; query.page = 1" :aria-pressed="!query?.status">All <span class="count">{{ fmt?.number(total) }}</span></button>
|
||||
<button type="button" class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1" :aria-pressed="!!(query?.status == 'active')">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1" :aria-pressed="!!(query?.status == 'banned')">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></button>
|
||||
<button type="button" class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1" :aria-pressed="!!(query?.status == 'removed')">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
<div class="seg-bar">
|
||||
<div class="seg-filter" role="radiogroup" aria-label="Filter by status">
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: !query?.status }" :aria-checked="!query?.status" @click="query.status = ''; query.page = 1">All <span class="count">{{ fmt?.number(statusCountFor('active') + statusCountFor('banned') + statusCountFor('removed')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: query?.status === 'active', 'is-empty': !statusCountFor('active') }" :aria-checked="query?.status === 'active'" @click="query.status = 'active'; query.page = 1"><span class="seg-dot ok" aria-hidden="true"></span>Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: query?.status === 'banned', 'is-empty': !statusCountFor('banned') }" :aria-checked="query?.status === 'banned'" @click="query.status = 'banned'; query.page = 1"><span class="seg-dot bad" aria-hidden="true"></span>Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></button>
|
||||
<button type="button" role="radio" class="seg-chip" :class="{ active: query?.status === 'removed', 'is-empty': !statusCountFor('removed') }" :aria-checked="query?.status === 'removed'" @click="query.status = 'removed'; query.page = 1"><span class="seg-dot off" aria-hidden="true"></span>Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
<p class="seg-countline" role="status"><strong>{{ total >= 0 ? fmt.number(total) : '…' }}</strong> shown</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
@@ -77,9 +79,9 @@
|
||||
<img width="28" height="28" class="rounded-circle" style="flex-shrink:0;" v-if="u?.photo" :src="safeUrl((u?.photo))">
|
||||
<span class="avatar-fallback" aria-hidden="true" v-else>{{ (u?.username || '?').charAt(0).toUpperCase() }}</span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="u?.username" :href="safeUrl("/admin/users/" + (u?.username))"></a>
|
||||
<div class="anon-sub">
|
||||
<span v-if="u?.emails?.[0]?.email">{{ u?.emails?.[0]?.email }} · </span><a target="_blank" :href="safeUrl("https://github.com/" + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry"> · Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</span>
|
||||
<span class="user-name-line"><a class="repo-name" v-text="u?.username" :href="safeUrl("/admin/users/" + (u?.username))"></a><a class="user-gh-link" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (u?.username))" :title="(u?.username) + ' on GitHub'" :aria-label="(u?.username) + ' on GitHub'"><i class="fab fa-github" aria-hidden="true"></i></a></span>
|
||||
<div class="anon-sub meta-dots">
|
||||
<span v-if="u?.emails?.[0]?.email" class="meta-email meta-tail" :title="u?.emails?.[0]?.email">{{ u?.emails?.[0]?.email }}</span><span v-if="u?.dateOfEntry">Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +89,7 @@
|
||||
<a :title="u?.repoCount ? 'Show this user’s repositories' : 'Repositories not synced yet'" v-text="u?.repoCount ? fmt?.number(u.repoCount) : 'Not synced'" :class="{ 'is-unsynced': !u?.repoCount }" :href="safeUrl("/admin/repositories?owner=" + (u?.username))"></a>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
<span class="status-dot" :class="{'status-ready': u?.status == 'active', 'status-removed': u?.status == 'removed' || u?.status == 'banned'}"></span>
|
||||
<span class="status-dot" :class="'st-' + (u?.status || 'unknown')"></span>
|
||||
<span v-text="fmt?.title(u?.status)"></span>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
@@ -116,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<span aria-hidden="true"></span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
|
||||
+221
-153
@@ -2,11 +2,22 @@
|
||||
<!-- ===== STATE 1: No URL — centered input ===== -->
|
||||
<div class="anonymize-landing" v-show="!workspaceOpen && !isUpdate">
|
||||
<div class="anonymize-landing-inner">
|
||||
<div class="paper-crumbs">My work / <span class="here">New anonymization</span></div>
|
||||
<h1 class="paper-page-title">New <em>anonymization</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
Paste a repository, pull request, or gist URL. You’ll choose the terms to redact next.
|
||||
</p>
|
||||
<header class="page-head">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">New anonymization</span></div>
|
||||
<div class="page-head-main"><div>
|
||||
<h1 class="paper-page-title">New anonymization</h1>
|
||||
<p class="paper-page-lede">Paste a repository, pull request, or gist URL, or pick one of your repositories. You’ll choose the terms to redact next.</p>
|
||||
</div></div>
|
||||
<ol class="task-trail flow-trail" aria-label="Anonymization steps"><li aria-current="step">1. Repository</li><li>2. Review and hosting</li><li>3. Review link</li></ol>
|
||||
</header>
|
||||
<aside class="task-summary landing-next" aria-labelledby="landing-next-title">
|
||||
<h2 id="landing-next-title">What happens next</h2>
|
||||
<ol>
|
||||
<li><b>Choose terms to redact.</b> Names, affiliations, and emails are replaced in every text file.</li>
|
||||
<li><b>Pick hosting and expiry.</b> Decide what reviewers may see and when the link stops working.</li>
|
||||
<li><b>Share the review link.</b> Reviewers open it without an account.</li>
|
||||
</ol>
|
||||
</aside>
|
||||
<div class="form-group mt-4 mb-2">
|
||||
<div class="source-url-label-row">
|
||||
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
|
||||
@@ -18,21 +29,43 @@
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ (detectedType === 'gist' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="landing-url-row"><input id="sourceUrl-landing" onblur="this.scrollLeft=0" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }"><button type="button" class="btn btn-primary landing-continue" :aria-disabled="!sourceUrl ? 'true' : null" @click="sourceUrl && urlSelected()">Continue</button></div>
|
||||
<div class="landing-url-row"><input id="sourceUrl-landing" onblur="this.scrollLeft=0" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }"><button type="button" class="btn btn-primary landing-continue" :aria-disabled="!sourceUrl?.trim() ? 'true' : null" @click="sourceUrl?.trim() && urlSelected()">Continue</button></div>
|
||||
</div>
|
||||
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
|
||||
<small class="form-text" style="color: var(--ink-muted);">
|
||||
<small class="form-text landing-help" style="color: var(--ink-muted);">
|
||||
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
||||
</small>
|
||||
<section class="repo-picker" aria-labelledby="repo-picker-title">
|
||||
<div class="repo-picker-head">
|
||||
<div class="repo-picker-title">
|
||||
<h2 id="repo-picker-title">Your repositories</h2>
|
||||
<span class="repo-picker-count" aria-live="polite" v-if="userReposStatus === 'ready' && userRepos.length > 0"><template v-if="userRepoFilter && filteredUserRepos().length !== userRepos.length">{{ filteredUserRepos().length }} of </template>{{ userRepos.length }} {{ userRepos.length === 1 ? 'repository' : 'repositories' }}<span class="repo-picker-more"><span class="repo-picker-more-sep"> · </span>scroll for more</span></span>
|
||||
</div>
|
||||
<input type="search" class="form-control repo-picker-filter" placeholder="Filter…" aria-label="Filter your repositories" autocomplete="off" v-model="userRepoFilter" v-if="userReposStatus === 'ready' && userRepos.length > 0">
|
||||
</div>
|
||||
<ul class="repo-picker-list" v-if="userReposStatus === 'loading' || userReposStatus === 'idle'" aria-busy="true" aria-label="Loading your repositories">
|
||||
<template v-for="(i, index) in [1, 2, 3, 4]"><li class="repo-picker-row is-skeleton" aria-hidden="true"><span class="skeleton skeleton-line" :style="{ width: (34 + i * 9) + '%' }"></span></li></template>
|
||||
</ul>
|
||||
<ul class="repo-picker-list" v-if="userReposStatus === 'ready' && filteredUserRepos().length > 0">
|
||||
<template v-for="(repo, index) in filteredUserRepos()" :key="repo.id || repo.fullName"><li class="repo-picker-row">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<span class="repo-picker-name" :title="repo.fullName"><span class="repo-picker-owner">{{ repo.fullName.split('/')[0] }}<span class="repo-picker-slash">/</span></span><span class="repo-picker-repo">{{ repo.fullName.split('/').slice(1).join('/') }}</span></span>
|
||||
<button type="button" class="btn btn-outline-ink btn-sm repo-picker-select" @click="selectUserRepo(repo)" :aria-label="'Select ' + repo.fullName">Select</button>
|
||||
</li></template>
|
||||
</ul>
|
||||
<p class="repo-picker-state" v-if="userReposStatus === 'ready' && userRepos.length > 0 && filteredUserRepos().length === 0">No repository matches “{{ userRepoFilter }}”.</p>
|
||||
<p class="repo-picker-state" v-if="userReposStatus === 'ready' && userRepos.length === 0">No repositories found for your GitHub account. Paste a URL above, or grant access below.</p>
|
||||
<p class="repo-picker-state" role="alert" v-if="userReposStatus === 'error'">Your repositories could not be loaded. <button type="button" class="task-text-button" @click="loadUserRepos(true)">Try again</button></p>
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage access to select this repository, then return here. Organization access may require administrator approval.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Anonymized content is public, even from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Anonymized content is public, even from private repositories.</span></div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +73,7 @@
|
||||
<div class="anonymize-workspace" v-show="workspaceOpen || isUpdate">
|
||||
<header class="anonymize-topbar">
|
||||
<div class="anonymize-topbar-inner">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">{{ isUpdate ? 'Edit anonymization' : 'New anonymization' }}</span></div>
|
||||
<div class="anonymize-topbar-head">
|
||||
<h1 class="paper-page-title anonymize-topbar-title">
|
||||
<span v-if="!isUpdate">New anonymization</span>
|
||||
@@ -49,6 +83,8 @@
|
||||
{{ detectedType === 'repo' ? 'Repository' : detectedType === 'pr' ? 'Pull request' : 'Gist' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="paper-page-lede anonymize-topbar-lede">{{ isUpdate ? 'Changes apply to the existing anonymous link; reviewers keep the same URL.' : 'Choose the terms to redact, what reviewers see, and when the link expires.' }}</p>
|
||||
<ol class="task-trail flow-trail" aria-label="Anonymization steps" v-if="!isUpdate"><li>1. Repository</li><li aria-current="step">2. Review and hosting</li><li>3. Review link</li></ol>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -67,7 +103,6 @@
|
||||
<button id="workspace-settings-tab" role="tab" type="button" :aria-selected="mobilePane === 'settings'" aria-controls="workspace-settings" :tabindex="mobilePane === 'settings' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('settings')">Settings</button>
|
||||
<button id="workspace-preview-tab" role="tab" type="button" :aria-selected="mobilePane === 'preview'" aria-controls="workspace-preview" :tabindex="mobilePane === 'preview' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('preview')">Reviewer preview</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-empty" id="workspace-preview" v-if="detectedType !== 'repo' && !details" role="status">{{ sourceLoading ? 'Loading source…' : 'Choose an accessible source to see its preview.' }}</div>
|
||||
<div class="anonymize-split">
|
||||
<!-- Form column (left) -->
|
||||
<div id="workspace-settings" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-settings-tab' : null" class="anonymize-form-col overflow-auto">
|
||||
@@ -88,6 +123,7 @@
|
||||
</aside>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" onblur="this.scrollLeft=0" :disabled="isUpdate && detectedType !== 'repo'" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }" :class="{'is-invalid': anonymize?.sourceUrl?.invalid}">
|
||||
<p class="source-summary" v-if="sourceSummary() && !anonymize?.sourceUrl?.invalid"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg>{{ sourceSummary() }}</p>
|
||||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.github">
|
||||
Please provide a valid GitHub URL.
|
||||
</div>
|
||||
@@ -114,32 +150,35 @@
|
||||
<p>You can review and update its settings before saving.</p>
|
||||
<div class="repo-access-controls">
|
||||
<a class="btn btn-ink" :href="existingConfigurationUrl()">Use existing configuration</a>
|
||||
<button type="button" class="btn btn-link" @click="previousConfigurations = []">Create a separate anonymization</button>
|
||||
<button type="button" class="btn btn-outline-ink" @click="previousConfigurations = []">Create a separate one</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage access to select this repository, then return here. Organization access may require administrator approval.</span><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
</aside>
|
||||
|
||||
<aside class="repo-access-details" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && accessRepairAllowed && githubConnections?.appEnabled && githubConnection === 'github-app' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||||
<p>If this repository became private or its App installation changed, repair its access here. This keeps the same anonymous URL and published commit.</p>
|
||||
<button type="button" class="btn btn-outline-ink" :disabled="repairingGitHubAccess" @click="repairGitHubAccess()">Repair GitHub access</button>
|
||||
<button type="button" class="btn btn-outline-ink" @click="grantGitHubAccess()">Manage access</button>
|
||||
<p v-if="accessRepairMessage" role="status">{{ accessRepairMessage }}</p>
|
||||
</aside>
|
||||
<details class="repair-disclosure" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && sourceUrl === 'https://github.com/' + _originalFullName" :open="!!(error || accessRepairMessage || reconnectRepositoryId)">
|
||||
<summary>Connection problems?</summary>
|
||||
<div class="repair-step" v-if="accessRepairAllowed && githubConnections?.appEnabled && githubConnection === 'github-app' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||||
<p>If this repository became private or its App installation changed, repair its access here. This keeps the same anonymous URL and published commit.</p>
|
||||
<button type="button" class="btn btn-outline-ink" :disabled="repairingGitHubAccess" @click="repairGitHubAccess()">Repair GitHub access</button>
|
||||
<button type="button" class="btn btn-outline-ink" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<p v-if="accessRepairMessage" role="status">{{ accessRepairMessage }}</p>
|
||||
</div>
|
||||
|
||||
<aside class="repo-access-details" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||||
<p>If you deleted and recreated this repository, reconnect it before choosing a branch.</p>
|
||||
<button type="button" class="btn btn-outline-ink" @click="reconnectSource()">Reconnect source repository</button>
|
||||
<p v-if="reconnectRepositoryId" role="status">Source reloaded. Review the branch, commit, and anonymization preview, then save to replace the published snapshot.</p>
|
||||
</aside>
|
||||
<div class="repair-step">
|
||||
<p>If you deleted and recreated this repository, reconnect it before choosing a branch.</p>
|
||||
<button type="button" class="btn btn-outline-ink" @click="reconnectSource()">Reconnect source repository</button>
|
||||
<p v-if="reconnectRepositoryId" role="status">Source reloaded. Review the branch, commit, and anonymization preview, then save to replace the published snapshot.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-grid-2" v-if="detectedType === 'repo'">
|
||||
<div class="form-group">
|
||||
@@ -182,7 +221,7 @@
|
||||
<div class="form-group" v-show="detectedType === 'repo'">
|
||||
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
|
||||
<input type="text" class="form-control" name="repoId" id="repoId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.repoId?.invalid}">
|
||||
<small class="form-text text-muted">anonymous.4open.science/r/{{ repoId }}</small>
|
||||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>r/<wbr><span class="anon-url-id">{{ repoId }}</span></small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.used">{{ repoId }} is already used.</div>
|
||||
</div>
|
||||
@@ -190,7 +229,7 @@
|
||||
<div class="form-group" v-show="detectedType === 'pr'">
|
||||
<label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label>
|
||||
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { pullRequestId = value }, value: pullRequestId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.pullRequestId?.invalid}">
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{ pullRequestId }}</code>.</small>
|
||||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>pr/<wbr><span class="anon-url-id">{{ pullRequestId }}</span></small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">{{ pullRequestId }} is already used.</div>
|
||||
</div>
|
||||
@@ -198,7 +237,7 @@
|
||||
<div class="form-group" v-show="detectedType === 'gist'">
|
||||
<label class="paper-field-label" for="gistId">Anonymized gist ID</label>
|
||||
<input type="text" class="form-control" name="gistId" id="gistId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { gistId = value }, value: gistId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.gistId?.invalid}">
|
||||
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{ gistId }}</code>.</small>
|
||||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>gist/<wbr><span class="anon-url-id">{{ gistId }}</span></small>
|
||||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.format">ID can only contain letters and numbers.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.used">{{ gistId }} is already used.</div>
|
||||
</div>
|
||||
@@ -280,7 +319,7 @@
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="notebook">Display Notebooks</label>
|
||||
<label class="form-check-label" for="notebook">Display notebooks</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "anonymize", options: {} }" :disabled="!details?.hasPage || (details?.pageSource && details?.pageSource?.branch !== source?.branch)">
|
||||
@@ -399,6 +438,7 @@
|
||||
<div class="hosting-date" v-if="options?.expirationMode !== 'never'">
|
||||
<label class="paper-field-label" for="expirationDate">Expires</label>
|
||||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value => { options.expirationDate = value }, value: options?.expirationDate, form: "anonymize", options: {} }" :class="{'is-invalid': anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
|
||||
<p class="date-echo" aria-live="polite" v-if="fmt?.dateEcho(options?.expirationDate)">Expires {{ fmt?.dateEcho(options?.expirationDate) }}</p>
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.min">Pick a date in the future.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.max">Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.</div>
|
||||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.required || anonymize?.expirationDate?.errors?.date">Enter a valid expiration date.</div>
|
||||
@@ -421,7 +461,7 @@
|
||||
|
||||
|
||||
<section class="paper-settings-section" v-show="isUpdate && detectedType === 'repo'">
|
||||
<h2 class="paper-section-eyebrow">Coauthors</h2>
|
||||
<h2 class="paper-section-eyebrow">Co-authors</h2>
|
||||
<p class="form-text text-muted" style="margin-bottom: 8px;">
|
||||
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
|
||||
</p>
|
||||
@@ -465,18 +505,20 @@
|
||||
<button type="submit" class="btn btn-ink" v-else :disabled="sourceLoading || billingBusy || hostingSaving || billingLoading || (hostingNeedsPayment() && ((!authorPlans().length && !hostingOneTime()) || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () => { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
|
||||
|
||||
</template>
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Pull Request
|
||||
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
Create anonymous link
|
||||
</button>
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Pull Request
|
||||
Save changes
|
||||
</button>
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Gist
|
||||
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
Create anonymous link
|
||||
</button>
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Gist
|
||||
Save changes
|
||||
</button>
|
||||
<p class="submit-reason" role="status" v-if="sourceLoading">{{ detectedType === 'pr' ? 'Waiting for the pull request to load from GitHub…' : detectedType === 'gist' ? 'Waiting for the gist to load from GitHub…' : 'Waiting for the repository to load from GitHub…' }}</p>
|
||||
<p class="submit-reason" role="status" v-else-if="!details && !isUpdate">Load an accessible source to continue. See the preview for details.</p>
|
||||
<p class="checkout-return-note" v-if="hostingNeedsPayment() && !billingPending">You pay in Stripe, then come back here to finish.</p>
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved ·" : "Unsaved changes ·" }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved" : "Discard" }}</button></div>
|
||||
@@ -484,127 +526,153 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview column (right) -->
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'repo'">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus !== 'ready' ? 'README preview' : (readmePreserved() ? 'README left unredacted (Unredacted files)' : (String(terms || '').trim() ? 'README with redactions applied' : 'No terms yet. Add some to see them redacted here.')) }}</span>
|
||||
<button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
||||
<!-- Preview column (right): "What reviewers will see", a miniature reviewer window -->
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col reviewer-preview" v-if="detectedType">
|
||||
<div class="rp-caption">
|
||||
<span class="rp-title">What reviewers will see</span>
|
||||
<span class="rp-count" v-if="previewStatus === 'ready' && previewRedactions() > 0" :title="previewRedactions() + ' redacted terms in this preview'"><span class="redact-slot" aria-hidden="true"><i>{{ previewRedactions() }}</i></span> redacted</span>
|
||||
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' && !String(terms || '').trim()">No terms yet</span>
|
||||
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' && readmePreserved()">README left unredacted</span>
|
||||
<span class="rp-spacer"></span>
|
||||
<a class="rp-open is-external" v-if="isUpdate && previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Open reviewer page <span aria-hidden="true">↗</span></a>
|
||||
<button type="button" class="rp-open is-toggle" v-else @click="previewExpanded = !previewExpanded" :aria-pressed="!!previewExpanded"><svg class="rp-open-icon" viewBox="0 0 16 16" aria-hidden="true"><path v-if="!previewExpanded" d="M9.5 2.5h4v4M13.5 2.5 9 7M6.5 13.5h-4v-4M2.5 13.5 7 9"/><path v-else d="M13.5 6.5h-4v-4M9.5 6.5 14 2M2.5 9.5h4v4M6.5 9.5 2 14"/></svg>{{ previewExpanded ? 'Back to settings' : 'Expand preview' }}</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" v-if="!sourceLoading && !readme && previewStatus === 'idle'">No README is available to preview. Review the published files before sharing.</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-if="previewStatus === 'ready'" v-html="renderRedacted(sanitize(html_readme))"></div>
|
||||
</div>
|
||||
<div class="rp-window">
|
||||
<div class="rp-chrome" aria-hidden="true"><i></i><i></i><i></i><span class="rp-url">{{ previewUrl() || 'anonymous.4open.science' }}</span></div>
|
||||
<div class="rp-bar" aria-hidden="true"><span class="vb-logo">Anonymous <em>GitHub</em></span><span class="rp-bar-id" v-if="previewId()">{{ previewId() }}</span><span class="rp-bar-kind" v-if="detectedType !== 'repo'">{{ detectedType === 'pr' ? 'Pull request' : 'Gist' }}</span></div>
|
||||
<div class="rp-body" :class="{ 'has-tree': detectedType === 'repo' }">
|
||||
<nav class="rp-tree" v-if="detectedType === 'repo'" aria-label="Files reviewers will see">
|
||||
<template v-if="previewTree().length">
|
||||
<span class="rp-tree-row" v-for="entry in previewTree()" :key="entry.name" :class="{ 'is-dir': entry.dir, 'is-active': entry.readme }"><svg class="rp-tree-icon" viewBox="0 0 16 16" aria-hidden="true"><path v-if="entry.dir" d="M1.5 3.5h4l1.5 1.5h7.5v8h-13z"/><path v-else d="M3.5 1.5h6l3 3v10h-9z"/></svg><span class="rp-tree-name">{{ entry.name }}</span></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="preserve.loading"><span class="rp-tree-row rp-skel-row" v-for="w in [62, 48, 70, 40]" :key="w"><i class="rp-skel" :style="{ width: w + '%' }"></i></span></template>
|
||||
<span class="rp-tree-label" v-if="!preserve.loading"><svg class="rp-tree-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M1.5 3.5h4l1.5 1.5h7.5v8h-13z"/></svg>Files</span>
|
||||
<span class="rp-tree-note" v-if="!preserve.loading">Every other file at this commit appears here, with the same redactions.</span>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="rp-doc">
|
||||
<div class="rp-file-head" v-if="detectedType === 'repo' && (previewStatus === 'ready' || readme)"><span class="rp-file-name">README.md</span><span class="rp-file-meta">Markdown</span></div>
|
||||
<!-- Loading: skeleton inside the window -->
|
||||
<div class="rp-state rp-loading" role="status" v-if="previewStatus === 'loading' || (sourceLoading && previewStatus !== 'ready' && previewStatus !== 'error')">
|
||||
<span class="rp-state-text">{{ previewStatus === 'loading' ? 'Updating the anonymized preview…' : (detectedType === 'pr' ? 'Loading the pull request from GitHub…' : detectedType === 'gist' ? 'Loading the gist from GitHub…' : 'Loading the repository from GitHub…') }}</span>
|
||||
<i class="rp-skel rp-skel-title"></i><i class="rp-skel" style="width:92%"></i><i class="rp-skel" style="width:86%"></i><i class="rp-skel" style="width:58%"></i>
|
||||
<i class="rp-skel rp-skel-block"></i><i class="rp-skel" style="width:80%"></i><i class="rp-skel" style="width:66%"></i>
|
||||
</div>
|
||||
<!-- The source itself could not be loaded -->
|
||||
<div class="rp-state rp-error" role="alert" v-else-if="!details && detectedType !== 'repo'">
|
||||
<strong>Couldn't load {{ sourceSummary() || (detectedType === 'pr' ? 'this pull request' : 'this gist') }}</strong>
|
||||
<p>{{ error || 'GitHub did not return it. Check that the link is right and that Anonymous GitHub can read it.' }}</p>
|
||||
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="detectedType === 'pr' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button><a class="btn btn-outline-ink btn-sm" v-else href="/connections">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a></div>
|
||||
</div>
|
||||
<div class="rp-state rp-error" role="alert" v-else-if="!details && detectedType === 'repo' && !isUpdate && error">
|
||||
<strong>Couldn't load this repository</strong>
|
||||
<p>{{ error }}</p>
|
||||
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button></div>
|
||||
</div>
|
||||
<!-- The anonymized preview failed -->
|
||||
<div class="rp-state rp-error" role="alert" v-else-if="previewStatus === 'error'">
|
||||
<strong>Couldn't apply the redactions</strong>
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<div class="rp-state-actions"><button class="btn btn-ink btn-sm" type="button" @click="retryPreview()">Retry preview</button></div>
|
||||
</div>
|
||||
<div class="rp-state rp-empty" v-else-if="detectedType === 'repo' && !readme && previewStatus === 'idle'">
|
||||
<strong>No README</strong>
|
||||
<p>Reviewers land on the file list. Open the files you will share and check them before sending the link.</p>
|
||||
</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-else-if="detectedType === 'repo' && previewStatus === 'ready'" v-html="renderRedacted(sanitize(html_readme))"></div>
|
||||
<div class="anonymize-preview-body" v-else-if="detectedType === 'gist' && previewStatus === 'ready'">
|
||||
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<strong>Gist preview</strong><button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
|
||||
<span class="badge" :class="{'badge-success': details?.gist?.isPublic, 'badge-secondary': !details?.gist?.isPublic}">
|
||||
{{ details?.gist?.isPublic ? 'public' : 'secret' }}
|
||||
</span>
|
||||
</h2>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
|
||||
</div>
|
||||
<small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
|
||||
<small v-if="options?.username && details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
|
||||
<div v-if="options?.content && previewGistFiles?.length">
|
||||
<ul class="pr-comments mt-3">
|
||||
<li class="pr-comment" v-for="(file, index) in previewGistFiles">
|
||||
<div class="pr-comment-head">
|
||||
<strong v-text="file?.filename"></strong>
|
||||
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
|
||||
</div>
|
||||
<gist-file :file="file" :terms="terms" :options="options"></gist-file>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="options?.comments && details?.gist?.comments && details?.gist?.comments?.length">
|
||||
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
|
||||
<span class="badge" :class="{'badge-success': details?.gist?.isPublic, 'badge-secondary': !details?.gist?.isPublic}">
|
||||
{{ details?.gist?.isPublic ? 'public' : 'secret' }}
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</h2>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
|
||||
</div>
|
||||
<small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
|
||||
<small v-if="options?.username && details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
|
||||
<div v-if="options?.content && previewGistFiles?.length">
|
||||
<ul class="pr-comments mt-3">
|
||||
<li class="pr-comment" v-for="(file, index) in previewGistFiles">
|
||||
<div class="pr-comment-head">
|
||||
<strong v-text="file?.filename"></strong>
|
||||
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
|
||||
</div>
|
||||
<gist-file :file="file" :terms="terms" :options="options"></gist-file>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="options?.comments && details?.gist?.comments && details?.gist?.comments?.length">
|
||||
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-else-if="detectedType === 'pr' && previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
|
||||
<span class="badge" :class="{'badge-success':details?.pullRequest?.merged, 'badge-warning':details?.pullRequest?.state=='open', 'badge-danger':details?.pullRequest?.state=='closed' && !details?.pullRequest?.merged}">
|
||||
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
|
||||
</span>
|
||||
</h2>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
|
||||
</div>
|
||||
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
|
||||
<div class="pr-body" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" id="preview-pr-diff-tab" :aria-selected="prTabState?.active === 'diff'" aria-controls="preview-pr-diff-panel" :tabindex="prTabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" id="preview-pr-comments-tab" :aria-selected="prTabState?.active === 'comments'" aria-controls="preview-pr-comments-panel" :tabindex="prTabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-status" v-if="!options?.diff && !options?.comments">Diff and comments are hidden by your display settings.</div>
|
||||
<div class="paper-tab-content">
|
||||
<div role="tabpanel" id="preview-pr-diff-panel" aria-labelledby="preview-pr-diff-tab" v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
|
||||
</div>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
<div role="tabpanel" id="preview-pr-comments-panel" aria-labelledby="preview-pr-comments-tab" v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<strong>Pull request preview</strong><button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
|
||||
<span class="badge" :class="{'badge-success':details?.pullRequest?.merged, 'badge-warning':details?.pullRequest?.state=='open', 'badge-danger':details?.pullRequest?.state=='closed' && !details?.pullRequest?.merged}">
|
||||
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
|
||||
</span>
|
||||
</h2>
|
||||
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
|
||||
</div>
|
||||
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
|
||||
<div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" id="preview-pr-diff-tab" :aria-selected="prTabState?.active === 'diff'" aria-controls="preview-pr-diff-panel" :tabindex="prTabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" id="preview-pr-comments-tab" :aria-selected="prTabState?.active === 'comments'" aria-controls="preview-pr-comments-panel" :tabindex="prTabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-status" v-if="!options?.diff && !options?.comments">Diff and comments are hidden by your display settings.</div>
|
||||
<div class="paper-tab-content">
|
||||
<div role="tabpanel" id="preview-pr-diff-panel" aria-labelledby="preview-pr-diff-tab" v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
|
||||
</div>
|
||||
<div role="tabpanel" id="preview-pr-comments-panel" aria-labelledby="preview-pr-comments-tab" v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
<div class="pr-comment-body" v-if="options?.body">
|
||||
<markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- More below: a fade with a way to keep reading (hidden by CSS once the window is scrolled to the end) -->
|
||||
<div class="rp-more" v-if="previewStatus === 'ready'">
|
||||
<a class="rp-more-btn" v-if="isUpdate && previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Continue reading on the reviewer page <span aria-hidden="true">↗</span></a>
|
||||
<button type="button" class="rp-more-btn" v-else-if="!previewExpanded" @click="previewExpanded = true">Continue reading in the expanded preview</button>
|
||||
<span class="rp-more-btn is-hint" v-else aria-hidden="true">Scroll for more</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<div class="form-group" id="expiration-date-form" v-show="!(options?.expirationMode=='never')">
|
||||
<label for="expirationDate">Expiration date of the anonymized repository</label>
|
||||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" v-field="{ state: viewState, set: value => { options.expirationDate = value }, value: options?.expirationDate, form: "anonymizeForm", options: {} }">
|
||||
<p class="date-echo" aria-live="polite" v-if="fmt?.dateEcho(options?.expirationDate)">Expires {{ fmt?.dateEcho(options?.expirationDate) }}</p>
|
||||
<small class="form-text text-muted" v-show="options?.expirationMode=='remove'">After {{ fmt?.date(options?.expirationDate) }}, the repository will be
|
||||
removed and the visitor will not be able to see the content of
|
||||
the repository.</small>
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
</div>
|
||||
<div class="d-flex align-items-end flex-wrap" style="gap: 12px; justify-content: space-between;">
|
||||
<div>
|
||||
<h1 class="paper-page-title" v-text="conference?.name"></h1>
|
||||
<div class="conf-title-row">
|
||||
<h1 class="paper-page-title" v-text="conference?.name"></h1>
|
||||
<span class="conf-status" v-if="conference?.status"><span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span><span v-text="fmt?.statusLabel(conference?.status)"></span></span>
|
||||
</div>
|
||||
<p class="paper-page-lede" v-if="conference?.url">
|
||||
<a target="_blank" rel="noopener" v-text="conference?.url" :href="safeUrl((conference?.url))"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-wrap" style="gap: 10px;">
|
||||
<span class="status-pill" :class="{'status-pill-ready': conference?.status == 'ready', 'status-pill-removed': conference?.status == 'removed' || conference?.status == 'expired'}">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span>
|
||||
<span v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</span>
|
||||
<a class="btn btn-outline-ink" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit conference</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,7 +22,7 @@
|
||||
<span>Review window <b>{{ fmt?.date(conference?.startDate, 'mediumDate') }} – {{ fmt?.date(conference?.endDate, 'mediumDate') }}</b></span>
|
||||
<span>Repositories <b>{{ fmt?.number(conference?.repositories?.length) }}</b></span>
|
||||
<span>Plan <b v-text="conferencePlanName(conference?.plan?.planID)"></b></span>
|
||||
<span v-if="conference?.price">Cost so far <b>{{ fmt?.number(conference?.price, 2) }} €</b></span>
|
||||
<span v-if="conference?.price">Cost so far <b>{{ fmt?.money(conference?.price) }}</b></span>
|
||||
</div>
|
||||
|
||||
<div class="conference-payment-status" v-if="billingNotice" role="status">
|
||||
@@ -42,11 +41,17 @@
|
||||
</div>
|
||||
<p><a href="/profile#settings-billing">Manage invoices</a></p>
|
||||
</section>
|
||||
<div class="paper-section-eyebrow">Repositories</div>
|
||||
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter repositories" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="dashboard-meta conf-view-meta">
|
||||
<div class="dashboard-view-tabs" role="group" aria-label="Repository view">
|
||||
<button type="button" :aria-pressed="confView() === 'current'" @click="setConfView('current')" title="Every repository except removed links">Current</button>
|
||||
<button type="button" v-if="confCount('attention') > 0 || confView() === 'attention'" :aria-pressed="confView() === 'attention'" @click="setConfView('attention')" :aria-label="'Needs attention ' + confCount('attention')"><span class="dashboard-attention-prefix">Needs </span><span>attention</span> <span class="attention-count" :class="{'has-attention': confCount('attention') > 0}">{{ fmt?.number(confCount('attention')) }}</span></button>
|
||||
<button type="button" v-if="confCount('removed') > 0 || confView() === 'removed'" :aria-pressed="confView() === 'removed'" @click="setConfView('removed')" title="Links reviewers can no longer open">Removed <span class="attention-count">{{ fmt?.number(confCount('removed')) }}</span></button>
|
||||
</div>
|
||||
<span class="dashboard-count" role="status">{{ search.trim() ? fmt?.number(filteredRepositories?.length || 0) + ' shown' : fmt?.number(filteredRepositories?.length || 0) + ((filteredRepositories?.length || 0) === 1 ? ' repository' : ' repositories') }}</span>
|
||||
</div>
|
||||
<form class="w-100 conf-repo-toolbar" aria-label="Filter repositories" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<input type="search" id="search" class="form-control" aria-label="Search repositories" placeholder="Search by ID or source repository…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
<input type="search" id="search" class="form-control" aria-label="Search repositories" placeholder="Search repositories…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<div class="dropdown">
|
||||
@@ -74,38 +79,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Status</span>
|
||||
<span class="filter-btn-value" v-if="filters?.status?.ready && filters?.status?.expired && filters?.status?.removed">All</span>
|
||||
<span class="filter-btn-value" v-if="!(filters?.status?.ready && filters?.status?.expired && filters?.status?.removed)">
|
||||
{{ (filters?.status?.ready ? 0 : 1) + (filters?.status?.expired ? 0 : 1) + (filters?.status?.removed ? 0 : 1) }} hidden
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Show statuses</h6>
|
||||
<div class="form-check dropdown-item" v-for="(label, key, index) in statusLabels">
|
||||
<input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value => { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id=""status-" + (key)">
|
||||
<label class="form-check-label" :for=""status-" + (key)">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="dashboard-meta">
|
||||
<span class="dashboard-count">
|
||||
<span v-if="filteredRepositories?.length === conference?.repositories?.length">{{ fmt?.number(conference?.repositories?.length) }} repositor<span v-if="conference?.repositories?.length === 1">y</span><span v-if="conference?.repositories?.length !== 1">ies</span></span>
|
||||
<span v-if="filteredRepositories?.length !== conference?.repositories?.length">{{ fmt?.number(filteredRepositories?.length) }} of {{ fmt?.number(conference?.repositories?.length) }} shown</span>
|
||||
</span>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
</div>
|
||||
|
||||
<div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories">
|
||||
<div class="dashboard-page conf-rows"><div class="paper-table paper-table-dashboard paper-table-conf w-100" role="table" aria-label="Repositories" :class="{'is-removed-view': confView() === 'removed'}">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader">Repository</div>
|
||||
<div role="columnheader">Status</div>
|
||||
@@ -115,42 +92,48 @@
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :key="repo?.repoId" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<span class="project-type-icon" role="img" aria-label="Repository" title="Repository"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg></span>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId) + "/")"></a>
|
||||
<div class="anon-title">
|
||||
<a class="repo-name" :title="repo?.source?.fullName" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><template v-if="(repo?.source?.fullName || '').indexOf('/') > 0"><span class="rn-owner">{{ repo.source.fullName.slice(0, repo.source.fullName.indexOf('/') + 1) }}</span><wbr><span class="rn-repo">{{ repo.source.fullName.slice(repo.source.fullName.indexOf('/') + 1) }}</span></template><template v-else>{{ repo?.source?.fullName || repo?.repoId }}</template></a>
|
||||
</div>
|
||||
<div class="anon-sub">
|
||||
<span class="anon-source">
|
||||
<a target="_blank" rel="noopener" v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update && repo?.source?.branch"> · <a target="_blank" rel="noopener" v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="repo?.source?.commit"> · <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))">@{{ repo?.source?.commit?.substring(0, 8) }}</a></span>
|
||||
</span>
|
||||
<code class="anonymous-id" :title="repo?.repoId"><template v-if="(repo?.repoId || '').length > 14"><span class="id-head">{{ repo.repoId.slice(0, -6) }}</span><span class="id-tail">{{ repo.repoId.slice(-6) }}</span></template><template v-else>{{ repo?.repoId }}</template></code>
|
||||
<span class="commit-hash" v-if="repo?.source?.commit" :title="repo.source.commit">{{ repo.source.commit.substring(0, 7) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-row-meta" role="presentation">
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + (repo?.status == 'error' ? 'error' : repo?.status == 'ready' ? 'ready' : (repo?.status == 'expired' || repo?.status == 'removed') ? repo?.status : 'progress')"></span>
|
||||
<span class="status-word" v-text="fmt?.statusLabel(repo?.status)"></span>
|
||||
</div>
|
||||
<div class="status-sub" v-if="repo?.anonymizeDate">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
|
||||
<div class="status-sub status-sub-error" v-if="repo?.status == 'error' && repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="repo?.statusMessage"></div>
|
||||
<a class="project-attention-action" v-if="repo?.status == 'error'" :href="safeUrl('/status/' + repo?.repoId)">View status</a>
|
||||
<button type="button" class="project-attention-action" v-if="repo?.status == 'expired'" @click.stop="extendRepo(repo)" :disabled="extendingId === repo?.repoId" title="Reviewers can no longer open this link">Extend 6 months</button>
|
||||
<div class="status-sub status-sub-date" v-if="repo?.anonymizeDate && repo?.status != 'error' && repo?.status != 'expired'">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="repo?.status == 'ready' && (repo?.options?.expirationMode === 'never' || !repo?.options?.expirationDate)">Never</span>
|
||||
<span v-if="repo?.status == 'ready' && repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
|
||||
<span class="expires-past" v-if="repo?.status == 'expired'">Expired<span v-if="repo?.options?.expirationDate"> {{ fmt?.humanTime(repo?.options?.expirationDate) }}</span></span>
|
||||
<div class="cell-views num" role="cell" :data-unit="(repo?.pageView || 0) === 1 ? 'view' : 'views'" :aria-label="fmt?.number(repo?.pageView || 0) + ((repo?.pageView || 0) === 1 ? ' view' : ' views')"><svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg><span>{{ fmt?.number(repo?.pageView || 0) }}</span></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="repo?.status == 'ready' && (repo?.options?.expirationMode === 'never' || !repo?.options?.expirationDate)">No expiry</span>
|
||||
<span v-if="repo?.status == 'ready' && repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate" v-text="(fmt?.humanTime(repo?.options?.expirationDate) || '').replace(/^on /, '')"></span>
|
||||
<span class="expires-past" v-if="repo?.status == 'expired'"><span class="expires-past-prefix">Expired </span>{{ fmt?.shortDate(repo?.options?.expirationDate) }}</span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="repo?.status != 'ready' && repo?.status != 'expired'">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<button type="button" class="copy-url" v-if="repo?.status != 'expired' && repo?.status != 'removed'" @click.stop="copyRepoLink(repo)" :disabled="!!copyingId" :title="'Copy link: /r/' + repo?.repoId + '/'" :aria-label="'Copy anonymous link for ' + repo?.repoId"><span class="sr-only" aria-live="polite">{{ copyingId === repo?.repoId ? 'Link copied' : '' }}</span><svg class="project-type-svg" aria-hidden="true"><use :href="copyingId === repo?.repoId ? '#ui-icon-check' : '#project-icon-copy'"></use></svg><span class="copy-url-label" aria-hidden="true">{{ copyingId === repo?.repoId ? 'Copied' : 'Copy link' }}</span></button>
|
||||
<a class="row-edit row-open" v-if="repo?.status != 'expired' && repo?.status != 'removed'" :href="safeUrl("/r/" + (repo?.repoId) + "/")" target="_blank" rel="noopener" title="Open what reviewers see" :aria-label="'Open anonymous page for ' + repo?.repoId"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label=""Actions for " + (repo?.repoId)">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")">View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")" target="_blank" rel="noopener">Open source on GitHub</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,9 +143,9 @@
|
||||
<span v-if="!conference?.repositories?.length">No repository has been submitted to this conference yet.</span>
|
||||
<span v-if="conference?.repositories?.length">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions" v-if="conference?.repositories?.length">
|
||||
<button type="button" class="btn btn-outline-ink" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<button type="button" class="btn btn-outline-ink" @click="search = ''; setConfView('current')">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,12 @@
|
||||
<div class="container page paper-page conferences-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Conferences</span></div>
|
||||
<div class="d-flex align-items-start justify-content-between flex-wrap page-head-row" style="gap: 12px;">
|
||||
<div class="d-flex align-items-start justify-content-between flex-wrap page-head-row page-head page-head-main" style="gap: 12px;">
|
||||
<div>
|
||||
<h1 class="paper-page-title">Your <em>conferences</em></h1>
|
||||
<p class="paper-page-lede">Group anonymizations by venue, give chairs a shared dashboard, and set one expiry for every submission.</p>
|
||||
</div>
|
||||
<a href="/conference/new" class="btn btn-primary">
|
||||
<a href="/conference/new" class="btn btn-primary" v-if="!(conferences && conferences.length == 0)">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference
|
||||
</a>
|
||||
</div>
|
||||
@@ -124,10 +123,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="!loading && !error && filteredConferences?.length == 0">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span class="empty-icon" aria-hidden="true" v-if="conferences?.length == 0"><svg class="ui-icon"><use href="#project-icon-conference"></use></svg></span>
|
||||
<svg class="ui-icon" aria-hidden="true" v-if="conferences?.length > 0"><use href="#project-icon-inbox"></use></svg>
|
||||
<template v-if="conferences?.length == 0"><strong class="empty-title">No conferences yet</strong><span>Create one to group submissions for a venue, share a dashboard with chairs, and set a single expiry date.</span></template>
|
||||
<span v-if="conferences?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions"><a href="/conference/new" class="btn btn-primary" v-if="false">New conference</a>
|
||||
<div class="paper-table-empty-actions"><a href="/conference/new" class="btn btn-primary conf-empty-new" v-if="conferences?.length == 0"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference</a>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="conferences?.length > 0" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<div class="container page dashboard-page paper-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="dashboard-heading">
|
||||
<div class="dashboard-heading page-head page-head-main">
|
||||
<div>
|
||||
<h1 class="paper-page-title">My work</h1>
|
||||
<div class="dashboard-title-line"><h1 class="paper-page-title">My work</h1><span class="dashboard-count-head" v-if="!loading && isPlainView()" aria-hidden="true">{{ fmt?.number(dashboardTotals?.filtered ?? filteredItems?.length) }} project<span v-if="(dashboardTotals?.filtered ?? filteredItems?.length) !== 1">s</span></span></div>
|
||||
<p class="paper-page-lede">Manage your anonymous links.</p>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="dashboard-heading-actions">
|
||||
<details class="dashboard-quota" :open="quotaExpanded" @toggle="quotaExpanded = $event.target.open" @keydown.esc.prevent.stop="closeQuota()">
|
||||
<summary aria-controls="dashboard-usage" :aria-expanded="quotaExpanded" :style="quota && !quota.repository.unlimited ? { '--quota-pct': quota.repository.percent + '%' } : null">
|
||||
<summary aria-controls="dashboard-usage" :aria-expanded="quotaExpanded" :class="quota ? 'quota-level-' + quota.repository.level : ''" :style="quota && !quota.repository.unlimited ? { '--quota-pct': quota.repository.percent + '%' } : null">
|
||||
<svg class="quota-summary-ring" viewBox="0 0 24 24" aria-hidden="true" :class="quota ? 'quota-' + quota.repository.level : ''">
|
||||
<circle class="quota-ring-track" cx="12" cy="12" r="9"></circle>
|
||||
<circle class="quota-ring-fill" cx="12" cy="12" r="9" pathLength="100" v-if="quota && !quota.repository.unlimited" :stroke-dasharray="quota.repository.percent + ' 100'"></circle>
|
||||
@@ -33,6 +33,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result count + active filter chips -->
|
||||
<div class="dashboard-meta" v-if="!loading">
|
||||
<div class="dashboard-view-tabs" role="group" aria-label="Project view" v-if="showViewTabs()">
|
||||
<button type="button" :aria-pressed="statusView() === 'active'" @click="setStatusView('active')" title="Every project except removed links">Current</button>
|
||||
<button type="button" v-if="attentionCount() > 0 || statusView() === 'attention'" :aria-pressed="statusView() === 'attention'" @click="setStatusView('attention')" :aria-label="'Needs attention ' + attentionCount()"><span class="dashboard-attention-prefix">Needs </span><span>attention</span> <span class="attention-count" :class="{'has-attention': attentionCount() > 0}">{{ attentionCount() }}</span></button>
|
||||
<button type="button" v-if="removedCount > 0 || statusView() === 'removed' || (removedCount == null && dashboardTotals?.total > dashboardTotals?.filtered)" :aria-pressed="statusView() === 'removed'" @click="setStatusView('removed')" :aria-label="'Removed ' + (removedCount ?? '')" title="Links reviewers can no longer open">Removed <span class="attention-count" v-if="removedCount != null">{{ fmt?.number(removedCount) }}</span></button>
|
||||
</div>
|
||||
<span class="dashboard-count" :class="{'is-plain': isPlainView()}">
|
||||
<span v-if="isPlainView()">{{ fmt?.number(dashboardTotals?.filtered ?? filteredItems?.length) }} project<span v-if="(dashboardTotals?.filtered ?? filteredItems?.length) !== 1">s</span></span>
|
||||
<span v-else>{{ fmt?.number(dashboardTotals?.filtered ?? filteredItems?.length) }} shown</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters -->
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
@@ -69,45 +82,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
|
||||
<div class="dropdown dashboard-status-filter" :class="{'is-view-hidden': statusView() === 'removed'}" :inert="statusView() === 'removed' ? true : null">
|
||||
<button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="filter-btn-label">Status</span>
|
||||
<span class="filter-btn-value" v-if="!hasHiddenStatus()">All</span>
|
||||
<span class="filter-btn-value" v-if="hiddenStatusCount() === 1 && filters?.status?.removed === false">Active</span>
|
||||
<span class="filter-btn-value" v-if="hasHiddenStatus() && !(hiddenStatusCount() === 1 && filters?.status?.removed === false)">{{ hiddenStatusCount() === 1 ? 'Hiding ' + (statusKeyLabels[Object.keys(filters?.status || {}).find(k => filters.status[k] === false)] || '').toLowerCase() : hiddenStatusCount() + ' hidden' }}</span>
|
||||
<span class="filter-btn-value">{{ statusChipKeys().length === 0 ? 'Any' : statusChipKeys().length === 1 ? 'Hiding ' + (statusKeyLabels[statusChipKeys()[0]] || '').toLowerCase() : statusChipKeys().length + ' hidden' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
|
||||
<h6 class="dropdown-header">Show statuses</h6>
|
||||
<div class="form-check dropdown-item" v-for="(label, key, index) in statusKeyLabels">
|
||||
<button type="button" class="dropdown-item status-preset" :class="{active: statusChipKeys().length === 0}" @click="statusChipKeys().forEach(k => { filters.status[k] = true })">Any status</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<template v-for="(label, key, index) in statusKeyLabels" :key="key"><div class="form-check dropdown-item" v-if="key !== 'removed'">
|
||||
<input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value => { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id=""status-" + (key)">
|
||||
<label class="form-check-label" :for=""status-" + (key)">{{ label }}</label>
|
||||
</div>
|
||||
</div></template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Result count + active filter chips -->
|
||||
<div class="dashboard-meta" v-if="!loading">
|
||||
<div class="dashboard-view-tabs" role="group" aria-label="Project view" v-if="attentionCount() > 0 || projectView === 'attention'">
|
||||
<button type="button" :aria-pressed="projectView === 'all'" @click="setProjectView('all')" aria-label="All projects"><span class="dashboard-view-long">All projects</span><span class="dashboard-view-short" aria-hidden="true">All</span></button>
|
||||
<button type="button" v-if="attentionCount() > 0 || projectView === 'attention'" :aria-pressed="projectView === 'attention'" @click="setProjectView('attention')" :aria-label="'Needs attention ' + attentionCount()"><span class="dashboard-attention-prefix">Needs </span><span>attention</span> <span class="attention-count" :class="{'has-attention': attentionCount() > 0}">{{ attentionCount() }}</span></button>
|
||||
</div>
|
||||
<span class="dashboard-count">
|
||||
<span v-if="(dashboardTotals?.filtered ?? filteredItems?.length) === (dashboardTotals?.total ?? items?.length)">{{ fmt?.number(dashboardTotals?.total ?? items?.length) }} project<span v-if="(dashboardTotals?.total ?? items?.length) !== 1">s</span></span>
|
||||
<span v-else>{{ fmt?.number(dashboardTotals?.filtered ?? filteredItems?.length) }} shown · <button type="button" class="btn-link-inline hidden-toggle" @click="clearFilters()">Show {{ fmt?.number((dashboardTotals?.total ?? items?.length) - (dashboardTotals?.filtered ?? filteredItems?.length)) }} removed</button></span>
|
||||
</span>
|
||||
<div class="dashboard-active-filters">
|
||||
<template v-for="(v, f, index) in filters?.status"><button type="button" class="filter-chip" v-if="v === false && !(f === 'removed' && hiddenStatusCount() === 1)" @click="filters.status[f] = true" :aria-label="'Show ' + statusKeyLabels[f] + ' again'" :title="'Show ' + statusKeyLabels[f] + ' again'">Hiding {{ (statusKeyLabels[f] || '').toLowerCase() }}<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></template>
|
||||
<div class="dashboard-active-filters dashboard-active-filters-row" v-if="!loading && showClearFilters()">
|
||||
<template v-for="f in statusChipKeys()" :key="f"><button type="button" class="filter-chip" @click="filters.status[f] = true" :aria-label="'Show ' + statusKeyLabels[f] + ' again'" :title="'Show ' + statusKeyLabels[f] + ' again'">Hiding {{ (statusKeyLabels[f] || '').toLowerCase() }}<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></template>
|
||||
<button type="button" class="filter-chip" v-if="typeFilter !== 'all'" @click="typeFilter = 'all'" aria-label="Show all project types">{{ typeFilter === 'repo' ? 'Repositories' : typeFilter === 'pr' ? 'Pull requests' : 'Gists' }}<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
<button type="button" class="btn-link-inline" v-if="showClearFilters()" @click="clearFilters()">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="paper-table paper-table-dashboard" role="table" aria-label="Anonymizations" :aria-busy="(loading)">
|
||||
<div class="paper-table paper-table-dashboard" role="table" aria-label="Anonymizations" :aria-busy="(loading)" :class="{'is-removed-view': statusView() === 'removed'}">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('_label') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('_label')}" @click="setSort('_label')">
|
||||
@@ -146,54 +149,55 @@
|
||||
<span class="project-type-icon" role="img" :aria-label="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'" :title="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'"><svg class="project-type-svg" aria-hidden="true"><use :href="'#project-type-' + item._type"></use></svg></span>
|
||||
<div class="anon-text">
|
||||
<div class="anon-title">
|
||||
<a class="repo-name" v-if="item?._viewUrl" :title="item._label" v-text="item?._label" :href="safeUrl((item?._viewUrl))"></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._label"></span>
|
||||
<a class="repo-name" v-if="item?._viewUrl" :title="item._label" :href="safeUrl((item?._viewUrl))"><template v-if="(item?._label || '').indexOf('/') > 0"><span class="rn-owner">{{ item._label.slice(0, item._label.indexOf('/') + 1) }}</span><wbr><span class="rn-repo">{{ item._label.slice(item._label.indexOf('/') + 1) }}</span></template><template v-else>{{ item?._label }}</template></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl"><template v-if="(item?._label || '').indexOf('/') > 0"><span class="rn-owner">{{ item._label.slice(0, item._label.indexOf('/') + 1) }}</span><wbr><span class="rn-repo">{{ item._label.slice(item._label.indexOf('/') + 1) }}</span></template><template v-else>{{ item?._label }}</template></span>
|
||||
<span class="type-badge type-coauthor" title="You are a co-author on this anonymization" v-if="item?.role === 'coauthor'">Co-author</span>
|
||||
</div>
|
||||
<div class="anon-sub">
|
||||
<code class="anonymous-id" v-if="item?._id" :title="item._id">{{ item._id }}</code>
|
||||
<span class="commit-hash" v-if="item?._type === 'repo' && item?.source?.commit" :title="item.source.commit">{{ item.source.commit.substring(0, 8) }}</span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-conference"></use></svg> <span v-text="item?.conference"></span></span>
|
||||
<div class="anon-sub" v-if="item?._id || (item?._type === 'repo' && item?.source?.commit) || item?.conference">
|
||||
<code class="anonymous-id" v-if="item?._id" :title="item._id"><template v-if="item._id.length > 14"><span class="id-head">{{ item._id.slice(0, -6) }}</span><span class="id-tail">{{ item._id.slice(-6) }}</span></template><template v-else>{{ item._id }}</template></code>
|
||||
<span class="commit-hash" v-if="item?._type === 'repo' && item?.source?.commit" :title="item.source.commit">{{ item.source.commit.substring(0, 7) }}</span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="'Conference: ' + item.conference"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-conference"></use></svg><span class="conf-name" v-text="item?.conference"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-row-meta" role="presentation">
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line" :class="{ 'status-ended': (item?._statusKey == 'expired' || item?._statusKey == 'removed'), ['status-ended-' + item?._statusKey]: (item?._statusKey == 'expired' || item?._statusKey == 'removed') }">
|
||||
<span class="status-dot" aria-hidden="true" v-if="!(item?._statusKey == 'expired' || item?._statusKey == 'removed')" :class="'status-' + item?._statusKey"></span>
|
||||
<svg class="ui-icon status-ended-icon" aria-hidden="true" v-if="(item?._statusKey == 'expired' || item?._statusKey == 'removed')"><use :href="item?._statusKey == 'removed' ? '#ui-icon-trash' : '#ui-icon-hourglass'"></use></svg>
|
||||
<div class="status-line">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + item?._statusKey"></span>
|
||||
<span class="status-word" v-text="fmt?.statusLabel(item?.status)"></span>
|
||||
<template v-if="item?.status == 'removed'"><span class="seg-sep status-removed-sep" aria-hidden="true">·</span><button type="button" class="project-attention-action status-enable" @click.stop="refreshItem(item)" title="Reviewers can no longer open this link">Enable again</button></template>
|
||||
</div>
|
||||
<div class="status-sub status-sub-error" v-if="item?.status == 'error' && item?.statusMessage" v-text="fmt?.statusMsg(item?.statusMessage)" :title="(item?.statusMessage)"></div>
|
||||
<div class="status-sub status-sub-warn" v-if="item?._stale" :title=""Last activity " + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
|
||||
Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} · may be stuck
|
||||
{{ idleText(item) }}
|
||||
</div>
|
||||
<a class="project-attention-action" v-if="item.status === 'error' && item._editUrl" :href="safeUrl(item._editUrl)">Fix source</a>
|
||||
<button type="button" class="project-attention-action" v-if="item._stale || item._broken" @click.stop="showProjectDetails(item)">{{ item._broken ? 'Review details' : 'Check progress' }}</button>
|
||||
<div class="status-sub status-sub-date" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
<div class="status-action-line" v-if="item._stale || item._broken"><span class="status-stuck" v-if="item._stale && !item._broken">May be stuck<span aria-hidden="true"> · </span></span><button type="button" class="project-attention-action" v-if="item._stale || item._broken" @click.stop="showProjectDetails(item)" :data-project-actions="item._broken ? encodeURIComponent(item._type + ':' + item._id) : null" :aria-controls="item._broken ? projectDetailsId(item) : null" :aria-expanded="item._broken ? selectedProject === item : null">{{ item._broken ? 'Review details' : 'Check progress' }}</button></div>
|
||||
<button type="button" class="project-attention-action" v-if="item?.status == 'expired'" @click.stop="extendItem(item)" title="Reviewers can no longer open this link">Extend 6 months</button>
|
||||
<div class="status-sub status-sub-date status-sub-removed" v-if="item?.status == 'removed' && item?.anonymizeDate">anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}</div>
|
||||
<div class="status-sub status-sub-date" v-if="item?.status != 'error' && item?.status != 'expired' && item?.status != 'removed' && !item?._stale && item?.anonymizeDate">
|
||||
anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" :aria-label="fmt.number(item.pageView) + ' views'"><svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg><span>{{ fmt.number(item.pageView) }}</span></div>
|
||||
<div class="cell-views num" role="cell" :data-unit="item.pageView === 1 ? 'view' : 'views'" :aria-label="fmt.number(item.pageView) + (item.pageView === 1 ? ' view' : ' views')"><svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg><span>{{ fmt.number(item.pageView) }}</span></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="item?._expiry?.kind === 'never'">Never</span>
|
||||
<span v-if="item?._expiry?.kind === 'date'" v-text="(fmt?.humanTime(item?._expiry?.date) || '').replace(/^on /, '')"></span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
|
||||
<span class="expires-never" v-if="item?._expiry?.kind === 'never'">No expiry</span>
|
||||
<span v-if="item?._expiry?.kind === 'date' && !item?._expiry?.soon" v-text="(fmt?.humanTime(item?._expiry?.date) || '').replace(/^on /, '')"></span>
|
||||
<span class="expires-soon" v-if="item?._expiry?.kind === 'date' && item?._expiry?.soon" :title="'Expires ' + fmt?.humanTime(item?._expiry?.date)"><svg class="project-type-svg expires-soon-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg><span class="expires-soon-prefix">Expires </span>{{ item._expiry.soon }}</span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && item?._expiry?.date"><span class="expires-past-prefix">Expired </span>{{ item?._expiry?.label || fmt?.humanTime(item?._expiry?.date) }}</span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && !item?._expiry?.date">Expired</span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === 'none'">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="ended-row" v-if="(item?._statusKey == 'expired' || item?._statusKey == 'removed')">
|
||||
<span class="ended-link" title="Reviewers can no longer open this link">Link offline</span>
|
||||
<button type="button" class="ended-action" v-if="item?.status == 'removed'" @click.stop="refreshItem(item)">Enable</button>
|
||||
<button type="button" class="ended-action" v-if="item?.status == 'expired'" @click.stop="extendItem(item)">Extend</button>
|
||||
</div>
|
||||
<button type="button" class="copy-url" v-if="item._viewUrl && !(item?._statusKey == 'expired' || item?._statusKey == 'removed')" @click.stop="copyLink(item)" :disabled="!!copyingId" :title="'Copy link: ' + item._viewUrl" :aria-label="'Copy anonymous link for ' + item._label"><span class="sr-only" aria-live="polite">{{ copyingId === item._type + ':' + item._id ? 'Link copied' : '' }}</span><svg class="project-type-svg" aria-hidden="true"><use :href="copyingId === item._type + ':' + item._id ? '#ui-icon-check' : '#project-icon-copy'"></use></svg></button>
|
||||
<button type="button" class="project-details-button" v-if="item._broken" @click.stop="showProjectDetails(item)" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-controls="projectDetailsId(item)" :aria-expanded="selectedProject === item">Details</button>
|
||||
<button type="button" class="copy-url" v-if="item._viewUrl && !(item?._statusKey == 'expired' || item?._statusKey == 'removed')" @click.stop="copyLink(item)" :disabled="!!copyingId" :title="'Copy link: ' + item._viewUrl" :aria-label="'Copy anonymous link for ' + item._label"><span class="sr-only" aria-live="polite">{{ copyingId === item._type + ':' + item._id ? 'Link copied' : '' }}</span><svg class="project-type-svg" aria-hidden="true"><use :href="copyingId === item._type + ':' + item._id ? '#ui-icon-check' : '#project-icon-copy'"></use></svg><span class="copy-url-label" aria-hidden="true">{{ copyingId === item._type + ':' + item._id ? 'Copied' : 'Copy link' }}</span></button>
|
||||
<a class="row-edit row-open" v-if="item._viewUrl && !(item?._statusKey == 'expired' || item?._statusKey == 'removed')" :href="safeUrl(item._viewUrl)" target="_blank" rel="noopener" :title="'Open what reviewers see'" :aria-label="'Open anonymous page for ' + item._label"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
<a class="row-edit" v-if="item?._editUrl && !item?._broken" :href="safeUrl(item._editUrl)" :title="'Edit ' + item._name" :aria-label="'Edit ' + item._name"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg></a>
|
||||
<a class="row-edit" v-if="item?._editUrl && !item?._broken && item?._statusKey != 'removed'" :href="safeUrl(item._editUrl)" :title="'Edit ' + item._name" :aria-label="'Edit ' + item._name"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg></a>
|
||||
<button type="button" class="copy-url is-disabled" v-if="!(item._viewUrl && !(item?._statusKey == 'expired' || item?._statusKey == 'removed')) && item?._statusKey != 'removed' && item?._statusKey != 'expired'" aria-disabled="true" @click.stop :title="(item?._statusKey == 'expired' ? 'Link expired: reviewers can no longer open it' : item?._statusKey == 'removed' ? 'Link offline: reviewers can no longer open it' : 'No link: this record is incomplete')" :aria-label="'Copy link unavailable for ' + item._label"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-copy"></use></svg><span class="copy-url-label" aria-hidden="true">Copy link</span></button>
|
||||
<button type="button" class="row-edit row-open is-disabled" v-if="!(item._viewUrl && !(item?._statusKey == 'expired' || item?._statusKey == 'removed')) && item?._statusKey != 'removed' && item?._statusKey != 'expired'" aria-disabled="true" @click.stop :title="(item?._statusKey == 'expired' ? 'Link expired: reviewers can no longer open it' : item?._statusKey == 'removed' ? 'Link offline: reviewers can no longer open it' : 'No link: this record is incomplete')" :aria-label="'Open unavailable for ' + item._label"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<button type="button" class="row-edit is-disabled" v-if="!(item?._editUrl && !item?._broken)" aria-disabled="true" @click.stop title="Can't edit: this record is incomplete" :aria-label="'Edit unavailable for ' + item._label"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg></button>
|
||||
<div class="dropdown is-disabled" v-if="item?._broken"><button type="button" class="btn btn-icon-dots is-disabled" aria-disabled="true" @click.stop title="No actions: this record is incomplete. Use Review details." :aria-label="'No actions available for ' + item._label"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-more"></use></svg></button></div>
|
||||
<div class="dropdown" v-if="!item?._broken">
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-label=""Actions for " + (item?._name)">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
@@ -264,6 +268,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-pagination" v-if="dashboardCursor || dashboardError">
|
||||
<p role="alert" v-if="dashboardError">{{ dashboardError }}</p>
|
||||
<button type="button" class="btn" v-if="dashboardCursor" @click="loadMoreProjects()" :disabled="loadingMore">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<div class="vb-left">
|
||||
<a class="vb-logo" href="/">Anonymous <em>GitHub</em></a>
|
||||
<button type="button" class="vb-icon vb-sidebar" :aria-label="sidebarHidden ? 'Show file tree' : 'Hide file tree'" :title="sidebarHidden ? 'Show file tree' : 'Hide file tree'" @click="sidebarHidden = !sidebarHidden"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sidebar"></use></svg></button>
|
||||
<a class="vb-artifact" v-if="repoId" :href="safeUrl('/r/' + repoId + '/')" :title="'Anonymous artifact ' + repoId"><span class="sr-only">Artifact </span><span class="vb-artifact-id">{{ repoId }}</span></a>
|
||||
</div>
|
||||
<div class="vb-right">
|
||||
<span class="vb-snapshot" v-if="options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate" :title="(options?.expirationDate ? 'Available until ' + fmt?.date(options.expirationDate) + '. ' : '') + ((options?.anonymizedAt && !options?.publishedAt) ? 'Older repositories have no recorded publication date. The source commit date does not establish when files became available here.' : ((options?.sourceCommitDate ? 'Source commit ' : options?.anonymizedAt ? 'Anonymized ' : 'Updated ') + fmt?.date(options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) + (options?.publishedAt ? ' · snapshot ready ' + fmt?.date(options.publishedAt) : '')))">
|
||||
@@ -32,6 +33,10 @@
|
||||
<a class="vb-signin" v-else href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i>Sign in</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="vb-context" v-if="repoId">
|
||||
<a class="vb-context-id" :href="safeUrl('/r/' + repoId + '/')" :title="'Anonymous artifact ' + repoId">{{ repoId }}</a>
|
||||
<span class="vb-context-date" v-if="options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate">Snapshot {{ fmt?.date(options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) }}</span>
|
||||
</div>
|
||||
|
||||
<button class="sidebar-toggle" v-show="files?.length" @click="sidebarCollapsed = !sidebarCollapsed" :aria-label="(sidebarCollapsed ? 'Show files' : 'Hide files')">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="sidebarCollapsed ? '#ui-icon-folder' : '#project-icon-close'"></use></svg>
|
||||
@@ -71,7 +76,7 @@
|
||||
<div class="leftCol-backdrop" v-show="files?.length && !sidebarCollapsed" @click="sidebarCollapsed = true"></div>
|
||||
|
||||
<div class="explorer-main">
|
||||
<div class="status-bar" v-show="type != 'error' || files?.length" :class="{ 'is-redundant': ['code', 'text', 'html'].includes(type) && content != null }">
|
||||
<div class="status-bar" v-show="type != 'error' || files?.length" :class="{ 'is-redundant': (['code', 'text', 'html'].includes(type) && content != null) || fileFrame() }">
|
||||
<button type="button" class="btn btn-sm status-files-btn" v-show="files?.length" aria-label="Show files" title="Show files" @click="sidebarHidden ? (sidebarHidden = false) : (sidebarCollapsed = false)">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-folder"></use></svg><span>Files</span>
|
||||
</button>
|
||||
|
||||
+30
-22
@@ -14,12 +14,28 @@
|
||||
<a href="#faq-plans">Plans and billing</a>
|
||||
<a href="#faq-features">Features</a>
|
||||
<a href="#faq-limitations">Limitations</a>
|
||||
<a href="#faq-privacy">Privacy & Security</a>
|
||||
<a href="#faq-hosting">Self-Hosting</a>
|
||||
<a href="#faq-privacy">Privacy and security</a>
|
||||
<a href="#faq-hosting">Self-hosting</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<section class="faq-section" aria-label="FAQs">
|
||||
<div>
|
||||
<section class="faq-section" aria-label="FAQs" :class="{ 'faq-searching': !!faqQuery.trim() }">
|
||||
<div class="faq-search" role="search">
|
||||
<label for="faq-search-input">Search questions</label>
|
||||
<div class="faq-search-field">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
|
||||
<input id="faq-search-input" type="search" class="form-control" v-model="faqQuery" @input="filterFaq()" @search="filterFaq()" @keydown.esc="clearFaqSearch()" placeholder="e.g. expiry, pull request, PDF" autocomplete="off" spellcheck="false" aria-describedby="faq-search-status">
|
||||
</div>
|
||||
<p id="faq-search-status" class="faq-search-status" aria-live="polite"><template v-if="faqQuery.trim() && faqResults">{{ faqResults === 1 ? '1 result' : faqResults + ' results' }}</template></p>
|
||||
</div>
|
||||
<div class="faq-search-empty" v-if="faqQuery.trim() && faqResults === 0">
|
||||
<strong>No questions match “{{ faqQuery.trim() }}”.</strong>
|
||||
<p>Try a shorter word, or ask us directly.</p>
|
||||
<div class="faq-search-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink btn-sm" @click="clearFaqSearch()">Clear search</button>
|
||||
<a class="btn btn-link btn-sm" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml">Open an issue</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="faq-list">
|
||||
|
||||
<!-- General -->
|
||||
<h4 class="faq-cat">
|
||||
@@ -150,7 +166,7 @@
|
||||
<div id="freePlan" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingFreePlan">
|
||||
<div class="panel-body p-3">
|
||||
<p>The default free allowance is 5 repositories you own and 2 GiB of source files in total. Core anonymization and reviewer access remain free. You can manually refresh a repository within your allowance and choose a dated expiration up to 5 months ahead, then extend it again before it expires. Repositories submitted to a conference can stay online until the end of its review window.</p>
|
||||
<p>Repositories shared with you as a coauthor do not use your own slots. Gists and pull requests are not included in these repository quotas. The usage shown in your account is the allowance available on this instance.</p>
|
||||
<p>Repositories shared with you as a co-author do not use your own slots. Gists and pull requests are not included in these repository quotas. The usage shown in your account is the allowance available on this instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,13 +221,13 @@
|
||||
<div class="panel panel-default mb-3">
|
||||
<div class="panel-heading p-3" role="tab" id="headingPaidCoauthors">
|
||||
<h3 class="panel-title">
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-plans" href="#paidCoauthors" aria-expanded="false" aria-controls="paidCoauthors">Does every coauthor need to pay?</a>
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-plans" href="#paidCoauthors" aria-expanded="false" aria-controls="paidCoauthors">Does every co-author need to pay?</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="paidCoauthors" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPaidCoauthors">
|
||||
<div class="panel-body p-3">
|
||||
<p>No. Coauthors can collaborate without buying a subscription. A shared repository uses its owner's allowance, unless a repository pass or paid conference covers it. It does not consume each coauthor's personal allowance.</p>
|
||||
<p>A coauthor's subscription does not increase the owner's allowance. If the owner's available capacity is exhausted, a coauthor's refresh or edit may also be blocked. Reviewers never need a paid account.</p>
|
||||
<p>No. Co-authors can collaborate without buying a subscription. A shared repository uses its owner's allowance, unless a repository pass or paid conference covers it. It does not consume each co-author's personal allowance.</p>
|
||||
<p>A co-author's subscription does not increase the owner's allowance. If the owner's available capacity is exhausted, a co-author's refresh or edit may also be blocked. Reviewers never need a paid account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -481,9 +497,9 @@ anonymous_github</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<!-- Privacy and security -->
|
||||
<h4 class="faq-cat">
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>Privacy & Security
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>Privacy and security
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-privacy" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -543,9 +559,9 @@ anonymous_github</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Self-Hosting -->
|
||||
<!-- Self-hosting -->
|
||||
<h4 class="faq-cat">
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-storage"></use></svg>Self-Hosting
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-storage"></use></svg>Self<span class="serif-hy">-</span>hosting
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-hosting" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -579,16 +595,7 @@ anonymous_github</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5 mb-5 p-4" style="background-color: var(--hover-bg-color); border-radius: 8px;">
|
||||
<p class="mb-2" style="font-size: 1.1rem;">
|
||||
Still have questions?
|
||||
</p>
|
||||
<p class="text-muted mb-0">
|
||||
Open an issue on the
|
||||
<a href="https://github.com/tdurieux/anonymous_github/issues" target="_blank">GitHub repository</a>
|
||||
and we'll be happy to help.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -601,3 +608,4 @@ anonymous_github</code></pre>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<partial-view name="partials/footer.htm" :state="viewState"></partial-view>
|
||||
@@ -0,0 +1,34 @@
|
||||
<footer class="paper-footer">
|
||||
<div class="paper-footer-inner">
|
||||
<div class="paper-footer-brand">
|
||||
<div class="paper-footer-mark">Anonymous <em>GitHub</em></div>
|
||||
<p class="paper-footer-tag">
|
||||
A read-only, identity-stripped mirror of your repository, built for
|
||||
double-anonymous peer review.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Product</div>
|
||||
<a href="/anonymize">Anonymize</a>
|
||||
<a href="/dashboard" v-if="user">My work</a>
|
||||
<a href="/conferences" v-if="user">Conferences</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Project</div>
|
||||
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a>
|
||||
<a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a>
|
||||
<a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Support</a>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Contact</div>
|
||||
<a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a>
|
||||
<a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-footer-rule"></div>
|
||||
<div class="paper-footer-foot">
|
||||
<span>© {{ year || new Date().getFullYear() }} Anonymous GitHub · GPL-3.0 licensed</span>
|
||||
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
|
||||
</div>
|
||||
</footer>
|
||||
+23
-53
@@ -2,7 +2,7 @@
|
||||
<!-- Hero: copy on the left, the product on the right -->
|
||||
<section class="paper-hero">
|
||||
<div class="paper-hero-copy">
|
||||
<div class="paper-eyebrow">For double-blind peer review</div>
|
||||
<div class="paper-eyebrow">For double-anonymous peer review</div>
|
||||
<h1 class="hero-title">
|
||||
Share the code,<br>
|
||||
<span class="accent">not the author.</span>
|
||||
@@ -27,6 +27,7 @@
|
||||
<li>Read-only access</li>
|
||||
<li>Set an expiry date</li>
|
||||
</ul>
|
||||
<p class="paper-proof-mobile">Used by authors at ICSE · FSE · OOPSLA</p>
|
||||
</div>
|
||||
<div class="paper-hero-shot">
|
||||
<div class="paper-frame">
|
||||
@@ -49,9 +50,9 @@
|
||||
<div class="hero-mock-doc-body">
|
||||
<div class="hero-mock-h1">DeepLearnUtils</div>
|
||||
<p>Developed by <span class="redact" role="img" aria-label="Redacted term 1"><i>1</i></span> at <span class="redact" role="img" aria-label="Redacted term 2"><i>2</i></span>.</p>
|
||||
<p>Utilities for reproducing the paper’s experiments.</p>
|
||||
<div class="hero-mock-h2">Quick start</div>
|
||||
<pre>pip install -r requirements.txt
|
||||
<p class="hm-opt">Utilities for reproducing the paper’s experiments.</p>
|
||||
<div class="hero-mock-h2 hm-opt">Quick start</div>
|
||||
<pre class="hm-opt">pip install -r requirements.txt
|
||||
python experiments/run.py</pre>
|
||||
<p>Contact: <span class="redact" role="img" aria-label="Redacted term 3"><i>3</i></span></p>
|
||||
</div>
|
||||
@@ -63,16 +64,16 @@ python experiments/run.py</pre>
|
||||
|
||||
|
||||
<!-- Live stats -->
|
||||
<section class="paper-stats paper-wrap" id="metrics" aria-label="Usage over the last 60 days">
|
||||
<section class="paper-stats paper-wrap" id="metrics" aria-label="Usage over the last 60 days" :class="{ 'is-empty': statsEmpty() }">
|
||||
<div class="paper-stats-meta">
|
||||
<div class="paper-stats-meta-left"><span class="paper-stats-dot" aria-hidden="true"></span> Live · last 60 days · updated daily</div>
|
||||
<div class="paper-stats-meta-left" v-if="!statsEmpty()"><span class="paper-stats-dot" aria-hidden="true"></span> <span>Live · last 60 days<span class="paper-stats-daily"> · updated daily</span></span></div>
|
||||
<div class="paper-stats-meta-right">Used by authors at ICSE · FSE · OOPSLA</div>
|
||||
</div>
|
||||
<div class="paper-stats-grid">
|
||||
<div class="paper-stats-grid" v-if="!statsEmpty()">
|
||||
<div class="paper-stat-card" v-for="(card, index) in cards" :key="card?.key">
|
||||
<div class="paper-stat-value" :class="{ 'is-pending': !stat }">{{ stat ? fmt?.bigNum(card?.total) : '—' }}</div>
|
||||
<div class="paper-stat-label">{{ card?.label }}</div>
|
||||
<div class="paper-stat-delta" v-if="history[card?.key].bars?.length > 1">
|
||||
<div class="paper-stat-delta" v-if="history[card?.key].bars?.length > 1 && history[card?.key].deltaToday > 0">
|
||||
+{{ fmt?.number(history[card?.key].deltaToday) }} today
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,8 +94,8 @@ python experiments/run.py</pre>
|
||||
</section>
|
||||
<div class="demo paper-wrap" aria-label="Try redaction">
|
||||
<div class="demo-terms">
|
||||
<label for="demo-terms">Terms to redact <span>comma-separated, try adding your own</span></label>
|
||||
<input id="demo-terms" type="text" v-model="demoTerms" spellcheck="false" autocomplete="off">
|
||||
<label for="demo-terms">Terms to redact <span>comma-separated<span class="dt-try">, try adding your own</span></span></label>
|
||||
<textarea id="demo-terms" class="demo-terms-input" rows="1" v-model="demoTerms" spellcheck="false" autocomplete="off" @keydown.enter.prevent></textarea>
|
||||
</div>
|
||||
<div class="demo-panes">
|
||||
<div class="pane">
|
||||
@@ -127,11 +128,11 @@ python experiments/run.py</pre>
|
||||
<div class="paper-features-list" role="tablist" aria-label="Features" aria-orientation="vertical">
|
||||
<div class="paper-feature" v-for="(f, index) in features" :key="f?.key" :class="{'is-on': feature === f?.key}">
|
||||
<button type="button" class="paper-feature-tab" role="tab" @click="selectFeature(f.key)" @keydown="featureKeydown($event, index)" :id=""feature-tab-" + (f?.key)" :aria-controls=""feature-panel-" + (f?.key)" :aria-selected="(feature === f?.key)" :tabindex="(feature === f?.key ? 0 : -1)">
|
||||
<span class="paper-eyebrow">{{ f?.num }} · {{ f?.eyebrow }}</span>
|
||||
<span class="paper-feature-title">{{ f?.title }} {{ f?.accent }}</span>
|
||||
<span class="paper-eyebrow"><span class="pf-num">{{ f?.num }}</span><span class="pf-sep"> · </span><span class="pf-name">{{ f?.eyebrow }}</span></span>
|
||||
<span class="paper-feature-title">{{ f?.title.split('-')[0] }}<span class="serif-hy" v-if="f?.title.includes('-')">-</span>{{ f?.title.split('-').slice(1).join('-') }} {{ f?.accent }}</span>
|
||||
</button>
|
||||
<div class="paper-feature-more">
|
||||
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{ f?.title }} {{ f?.accent }}</span>
|
||||
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{ f?.title.split('-')[0] }}<span class="serif-hy" v-if="f?.title.includes('-')">-</span>{{ f?.title.split('-').slice(1).join('-') }} {{ f?.accent }}</span>
|
||||
<p>{{ f?.text }}</p>
|
||||
<a class="paper-link-arrow" :href="safeUrl((featureHref(f)))" :target="(featureTarget(f))">{{ f?.cta }} →</a>
|
||||
</div>
|
||||
@@ -192,13 +193,15 @@ python experiments/run.py</pre>
|
||||
</section>
|
||||
|
||||
<!-- FAQ teaser -->
|
||||
<section class="paper-qa paper-wrap" aria-labelledby="qa-heading">
|
||||
<div>
|
||||
<div class="paper-eyebrow">Before you sign in</div>
|
||||
<h2 id="qa-heading">The questions that come up most.</h2>
|
||||
<section class="paper-qa paper-qa-r3 paper-wrap" aria-labelledby="qa-heading">
|
||||
<div class="qa-head">
|
||||
<div>
|
||||
<div class="paper-eyebrow">Before you sign in</div>
|
||||
<h2 id="qa-heading">The questions that come up most.</h2>
|
||||
</div>
|
||||
<a class="paper-link-arrow" href="/faq">Read the full FAQ →</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="qa-grid">
|
||||
<div class="q">
|
||||
<a href="/faq#permissions">What access does GitHub sign-in require?</a>
|
||||
<p>
|
||||
@@ -232,41 +235,8 @@ python experiments/run.py</pre>
|
||||
</div>
|
||||
<div class="paper-cta-actions">
|
||||
<a class="btn-hero" href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a>
|
||||
<a class="paper-link-arrow" href="/faq">Read the FAQ →</a>
|
||||
<a class="paper-link-arrow" target="_self" href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/">See an anonymized repository →</a>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="paper-footer">
|
||||
<div class="paper-footer-inner">
|
||||
<div class="paper-footer-brand">
|
||||
<div class="paper-footer-mark">Anonymous <em>GitHub</em></div>
|
||||
<p class="paper-footer-tag">
|
||||
A read-only, identity-stripped mirror of your repository, built for
|
||||
double-anonymous peer review.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Product</div>
|
||||
<a href="/anonymize">Anonymize</a>
|
||||
<a href="/dashboard" v-if="user">My work</a>
|
||||
<a href="/conferences" v-if="user">Conferences</a>
|
||||
<a href="/faq">FAQ</a>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Project</div>
|
||||
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a>
|
||||
<a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a>
|
||||
<a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Support</a>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
<div class="paper-footer-head">Contact</div>
|
||||
<a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a>
|
||||
<a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-footer-rule"></div>
|
||||
<div class="paper-footer-foot">
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · GPL-3.0 licensed</span>
|
||||
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
|
||||
</div>
|
||||
</footer>
|
||||
<partial-view name="partials/footer.htm" :state="viewState"></partial-view>
|
||||
</div>
|
||||
@@ -22,17 +22,18 @@
|
||||
<section class="file-card" :class="{ 'is-code': type != 'html', 'no-wrap': !wrapLines }" v-if="(type == 'text' || type == 'code' || type == 'html' || (type == 'html-doc' && showSource)) && content != null">
|
||||
<header class="file-card-head">
|
||||
<div class="file-card-path">
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost card-files-btn" :class="{ 'is-needed': sidebarHidden }" aria-label="Show file tree" title="Show file tree" @click="sidebarHidden ? (sidebarHidden = false) : (sidebarCollapsed = false)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sidebar"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost card-files-btn" :class="{ 'is-needed': sidebarHidden }" title="Show file tree" @click="sidebarHidden ? (sidebarHidden = false) : (sidebarCollapsed = false)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sidebar"></use></svg><span class="card-files-label">Files</span></button>
|
||||
<ol class="breadcrumb paths" aria-label="Path">
|
||||
<li class="breadcrumb-item breadcrumb-root"><a :href="safeUrl('/r/' + repoId + '/')" :title="repoId" v-text="/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(repoId || '') ? 'repository' : repoId"></a></li>
|
||||
<li class="breadcrumb-item" v-for="(p, index) in paths" :aria-current="index === paths.length - 1 ? 'page' : null"><template v-for="(seg, si) in redactParts(p)" :key="si"><span v-if="seg.n" class="redact-slot" role="img" :aria-label="'Redacted term ' + seg.n"><i>{{ seg.n }}</i></span><template v-else>{{ seg.t }}</template></template></li>
|
||||
</ol>
|
||||
<span class="mobile-sub"><span>{{ type == 'html' ? 'Markdown' : (languageName || 'Text') }}</span><span v-if="type != 'html' && typeof content === 'string'">{{ fmt?.number(content.split('\n').length) }} lines</span><span v-if="type == 'html' && fileSize(filePath) != null">{{ formatSize(fileSize(filePath)) }}</span><b v-if="redactionCount()">{{ redactionCount() }} redacted</b></span>
|
||||
<span class="mobile-sub ms"><b class="ms-item ms-redact" v-if="redactionCount()" :title="redactionCount() + ' redacted terms'">{{ redactionCount() }}<span class="meta-redact-word"> redacted</span></b><span class="ms-item">{{ type == 'html' ? 'Markdown' : (languageName || 'Text') }}</span><span class="ms-item" v-if="fileSize(filePath) != null">{{ formatSize(fileSize(filePath)) }}</span><span class="ms-item" v-if="type != 'html' && typeof content === 'string'">{{ fmt?.number(content.split('\n').length) }} lines</span></span>
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost copy-path-btn" v-show="filePath" :aria-label="pathCopied ? 'Path copied' : 'Copy file path'" :title="pathCopied ? 'Path copied' : 'Copy file path'" @click="copyPath()"><svg class="ui-icon" aria-hidden="true"><use :href="pathCopied ? '#ui-icon-check' : '#ui-icon-link'"></use></svg></button>
|
||||
</div>
|
||||
<span class="file-card-meta" v-if="type != 'html' && typeof content === 'string'"><template v-if="languageName">{{ languageName }}<span class="sep">·</span></template>{{ fmt?.number(content.split('\n').length) }} lines<span class="meta-extra"><span class="sep">·</span>{{ content.length < 1024 ? content.length + ' B' : (content.length / 1024).toFixed(1) + ' KB' }}</span><template v-if="redactionCount()"><span class="sep">·</span><span class="meta-redact">{{ redactionCount() }} redacted</span></template></span>
|
||||
<span class="file-card-meta" v-if="type == 'html'">Markdown<template v-if="fileSize(filePath) != null"><span class="sep">·</span>{{ formatSize(fileSize(filePath)) }}</template><template v-if="redactionCount()"><span class="sep">·</span><span class="meta-redact">{{ redactionCount() }} redacted</span></template></span>
|
||||
<span class="file-card-meta" v-if="type != 'html' && typeof content === 'string'"><template v-if="redactionCount()"><span class="meta-redact" :title="redactionCount() + ' redacted terms'">{{ redactionCount() }}<span class="meta-redact-word"> redacted</span></span><span class="sep">·</span></template>{{ languageName || 'Text' }}<span class="meta-extra"><span class="sep">·</span>{{ content.length < 1024 ? content.length + ' B' : (content.length / 1024).toFixed(1) + ' KB' }}</span><span class="sep">·</span>{{ fmt?.number(content.split('\n').length) }} lines</span>
|
||||
<span class="file-card-meta" v-if="type == 'html'"><template v-if="redactionCount()"><span class="meta-redact" :title="redactionCount() + ' redacted terms'">{{ redactionCount() }}<span class="meta-redact-word"> redacted</span></span><span class="sep">·</span></template>Markdown<template v-if="fileSize(filePath) != null"><span class="sep">·</span>{{ formatSize(fileSize(filePath)) }}</template></span>
|
||||
<div class="file-card-actions">
|
||||
<div class="csv-toggle seg-filter" role="radiogroup" aria-label="CSV view" v-if="isCsv()"><button type="button" role="radio" class="seg-chip" :class="{ active: csvView === 'table' }" :aria-checked="csvView === 'table'" @click="csvView = 'table'">Table</button><button type="button" role="radio" class="seg-chip" :class="{ active: csvView === 'raw' }" :aria-checked="csvView === 'raw'" @click="csvView = 'raw'" title="Show the file as text with line numbers">Text</button></div>
|
||||
<div class="symbols" v-if="type != 'html' && symbols().length > 1">
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-symbols" :class="{ 'is-on': symbolsOpen }" :aria-expanded="symbolsOpen ? 'true' : 'false'" aria-haspopup="true" title="Symbols in this file" @click="symbolsOpen = !symbolsOpen"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg><span class="btn-find-label">Symbols</span></button>
|
||||
<div class="symbols-menu" v-if="symbolsOpen" role="menu">
|
||||
@@ -41,9 +42,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-icon-only btn-find btn-md-find" v-if="type == 'html'" :class="{ 'is-on': mdFindOpen }" aria-label="Find on this page" :title="'Find on this page (' + (isMac ? '⌘' : 'Ctrl+') + 'F)'" @click="toggleMdFind()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-icon-only btn-find" v-if="type != 'html'" aria-label="Find in file" :title="'Find in file (' + (isMac ? '⌘' : 'Ctrl+') + 'F)'" @click="findInFile()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-icon-only btn-goto" v-if="type != 'html'" aria-label="Go to line" :title="'Go to line (' + (isMac ? '⌘' : 'Ctrl+') + 'G)'" @click="gotoLine()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-goto"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost btn-toggle" v-if="type != 'html'" :class="{ 'is-on': wrapLines }" :aria-pressed="wrapLines ? 'true' : 'false'" aria-label="Wrap long lines" :title="'Wrap long lines (Alt+Z)'" @click="toggleWrap()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-wrap"></use></svg></button>
|
||||
<div class="tb-view" role="group" aria-label="View tools" v-if="type != 'html' && !(isCsv() && csvView === 'table')">
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-icon-only btn-goto" v-if="type != 'html'" aria-label="Go to line" :title="'Go to line (' + (isMac ? '⌘' : 'Ctrl+') + 'G)'" @click="gotoLine()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-goto"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost btn-toggle" v-if="type != 'html'" :class="{ 'is-on': wrapLines }" :aria-pressed="wrapLines ? 'true' : 'false'" aria-label="Wrap lines" :title="'Wrap lines (Alt+Z)'" @click="toggleWrap()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-wrap"></use></svg></button>
|
||||
<button type="button" class="btn btn-sm btn-ghost btn-icon-only btn-find" v-if="type != 'html'" aria-label="Find in file" :title="'Find in file (' + (isMac ? '⌘' : 'Ctrl+') + 'F)'" @click="findInFile()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg></button>
|
||||
</div>
|
||||
<div class="tb-file" role="group" aria-label="File actions">
|
||||
<a target="_self" class="tb-seg-btn" :title="type == 'html' ? 'Open the Markdown source' : 'Open the raw file'" :href="safeUrl((url))">Raw</a>
|
||||
<button type="button" class="tb-seg-btn tb-seg-icon" v-if="typeof content === 'string'" :aria-label="contentCopied ? 'Copied' : 'Copy file contents'" :title="contentCopied ? 'Copied' : 'Copy file contents'" @click="copyContent()"><svg class="ui-icon" aria-hidden="true"><use :href="contentCopied ? '#ui-icon-check' : '#project-icon-copy'"></use></svg></button>
|
||||
@@ -62,13 +65,14 @@
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost tb-more-btn" :class="{ 'is-on': moreOpen }" aria-label="More actions" aria-haspopup="true" :aria-expanded="moreOpen ? 'true' : 'false'" @click.stop="tocOpen = false; moreOpen = !moreOpen"><svg viewBox="0 0 24 24" class="ui-icon" aria-hidden="true" style="fill:currentColor;stroke:none"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg></button>
|
||||
<div class="tb-menu" v-if="moreOpen" role="menu" @click="moreOpen = false">
|
||||
<div class="tb-menu-note" v-if="options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>Snapshot · {{ fmt?.date(options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) }}</div>
|
||||
<div class="tb-menu-note tb-menu-meta" v-if="typeof content === 'string'">{{ type == 'html' ? 'Markdown' : (languageName || 'Text') }}<template v-if="type != 'html'"> · {{ fmt?.number(content.split('\n').length) }} lines</template> · {{ type == 'html' && fileSize(filePath) != null ? formatSize(fileSize(filePath)) : (content.length < 1024 ? content.length + ' B' : (content.length / 1024).toFixed(1) + ' KB') }}</div>
|
||||
<div class="tb-menu-note tb-menu-meta" v-if="typeof content === 'string'">{{ type == 'html' ? 'Markdown' : (languageName || 'Text') }} · {{ type == 'html' && fileSize(filePath) != null ? formatSize(fileSize(filePath)) : (content.length < 1024 ? content.length + ' B' : (content.length / 1024).toFixed(1) + ' KB') }}<template v-if="type != 'html'"> · {{ fmt?.number(content.split('\n').length) }} lines</template></div>
|
||||
<div class="tb-menu-sep" v-if="options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate"></div>
|
||||
<button type="button" role="menuitem" v-if="type == 'html'" @click="toggleMdFind()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>Find in file</button>
|
||||
<div class="tb-menu-sep" v-if="type == 'html'"></div>
|
||||
<button type="button" role="menuitem" v-if="type != 'html'" @click="findInFile()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>Find in file</button>
|
||||
<button type="button" role="menuitem" v-if="type != 'html'" @click="gotoLine()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-goto"></use></svg>Go to line</button>
|
||||
<button type="button" role="menuitemcheckbox" v-if="type != 'html'" :aria-checked="wrapLines ? 'true' : 'false'" @click="toggleWrap()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-wrap"></use></svg>Wrap lines<span class="tb-menu-check" v-if="wrapLines">✓</span></button>
|
||||
<button type="button" role="menuitem" class="tb-csv-view" v-if="isCsv()" @click="csvView = csvView === 'table' ? 'raw' : 'table'; moreOpen = false"><svg class="ui-icon" aria-hidden="true"><use :href="csvView === 'table' ? '#ui-icon-code' : '#ui-icon-list'"></use></svg>{{ csvView === 'table' ? 'Show as text' : 'Show as table' }}</button>
|
||||
<div class="tb-menu-sep" v-if="type != 'html'"></div>
|
||||
<a role="menuitem" target="_self" v-if="type == 'html'" :href="safeUrl((url))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg>View raw</a>
|
||||
<a role="menuitem" target="_self" v-if="type != 'html'" :href="safeUrl((url))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg>View raw</a>
|
||||
@@ -89,7 +93,13 @@
|
||||
<span class="goto-hint" :class="{ 'is-error': gotoError }">{{ gotoError || 'Line, or line:column' }}</span>
|
||||
</form>
|
||||
</header>
|
||||
<div class="file-card-body" v-if="type != 'html'" v-code-editor="{ content: content, options: aceOption }"></div>
|
||||
<div class="csv-table-wrap" v-if="isCsv() && csvView === 'table'" tabindex="0" role="region" :aria-label="'Table view of ' + (paths?.[paths.length - 1] || 'file')">
|
||||
<table class="csv-table">
|
||||
<thead><tr><th class="csv-rownum" scope="col"><span class="sr-only">Row</span></th><th scope="col" v-for="(cell, ci) in (csvRows()[0] || [])" :key="ci" :class="{ 'is-num': csvNumCol(ci) }"><template v-for="(seg, si) in redactParts(cell)" :key="si"><span v-if="seg.n" class="redact-slot" role="img" :aria-label="'Redacted term ' + seg.n"><i>{{ seg.n }}</i></span><template v-else>{{ seg.t }}</template></template></th></tr></thead>
|
||||
<tbody><tr v-for="(row, ri) in csvRows().slice(1)" :key="ri"><td class="csv-rownum">{{ ri + 1 }}</td><td v-for="(cell, ci) in row" :key="ci" :class="{ 'is-num': csvNumCol(ci) }"><template v-for="(seg, si) in redactParts(cell)" :key="si"><span v-if="seg.n" class="redact-slot" role="img" :aria-label="'Redacted term ' + seg.n"><i>{{ seg.n }}</i></span><template v-else>{{ seg.t }}</template></template></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="file-card-body" v-if="type != 'html' && !(isCsv() && csvView === 'table')" v-code-editor="{ content: content, options: aceOption }"></div>
|
||||
<div class="md-find-bar" v-if="type == 'html' && mdFindOpen" role="search">
|
||||
<input type="search" class="md-find-input" placeholder="Find on this page" aria-label="Find on this page" :value="mdFindQuery" @input="mdFindQuery = $event.target.value; runMdFind()" @keydown.enter.prevent="$event.shiftKey ? stepMdFind(-1) : stepMdFind(1)" @keydown.esc.prevent="toggleMdFind(false)">
|
||||
<span class="md-find-nav"><button type="button" aria-label="Previous match" @click="stepMdFind(-1)" :disabled="!mdFindTotal"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button><button type="button" aria-label="Next match" @click="stepMdFind(1)" :disabled="!mdFindTotal"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button></span>
|
||||
@@ -106,11 +116,64 @@
|
||||
</div>
|
||||
</section>
|
||||
<html-doc v-if="type == 'html-doc' && !showSource && content != null" :content="content" :base-url="(fileBaseUrl)" :allow-scripts="allowScripts"></html-doc>
|
||||
<img class="image-content" v-if="type == 'image'" :src="safeUrl((url))">
|
||||
<iframe class="h-100 overflow-auto w-100 b-0" v-if="type == 'media'" :src="safeUrl((url))"></iframe>
|
||||
<pdfviewer v-if="type == 'pdf'" :src="safeUrl((url))"></pdfviewer>
|
||||
<div v-if="type == 'audio'"><audio controls="controls"><source :src="safeUrl((url))"></audio></div>
|
||||
<div v-if="type == 'IPython'"><notebook :file="url" :content="content"></notebook></div>
|
||||
<section class="file-card file-card-media" :class="'view-' + type" v-if="fileFrame()">
|
||||
<header class="file-card-head">
|
||||
<div class="file-card-path">
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost card-files-btn" :class="{ 'is-needed': sidebarHidden }" title="Show file tree" @click="sidebarHidden ? (sidebarHidden = false) : (sidebarCollapsed = false)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sidebar"></use></svg><span class="card-files-label">Files</span></button>
|
||||
<ol class="breadcrumb paths" aria-label="Path">
|
||||
<li class="breadcrumb-item breadcrumb-root"><a :href="safeUrl('/r/' + repoId + '/')" :title="repoId" v-text="/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(repoId || '') ? 'repository' : repoId"></a></li>
|
||||
<li class="breadcrumb-item" v-for="(p, index) in paths" :aria-current="index === paths.length - 1 ? 'page' : null"><template v-for="(seg, si) in redactParts(p)" :key="si"><span v-if="seg.n" class="redact-slot" role="img" :aria-label="'Redacted term ' + seg.n"><i>{{ seg.n }}</i></span><template v-else>{{ seg.t }}</template></template></li>
|
||||
</ol>
|
||||
<span class="mobile-sub ms"><b class="ms-item ms-redact" v-if="type == 'IPython' && redactionCount()" :title="redactionCount() + ' redacted terms'">{{ redactionCount() }}<span class="meta-redact-word"> redacted</span></b><span class="ms-item">{{ fileKind() }}</span><span class="ms-item" v-if="imageSize">{{ imageSize }}</span><span class="ms-item" v-if="fileSize(filePath) != null">{{ formatSize(fileSize(filePath)) }}</span></span>
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost copy-path-btn" v-show="filePath" :aria-label="pathCopied ? 'Path copied' : 'Copy file path'" :title="pathCopied ? 'Path copied' : 'Copy file path'" @click="copyPath()"><svg class="ui-icon" aria-hidden="true"><use :href="pathCopied ? '#ui-icon-check' : '#ui-icon-link'"></use></svg></button>
|
||||
</div>
|
||||
<span class="file-card-meta"><template v-if="type == 'IPython' && redactionCount()"><span class="meta-redact" :title="redactionCount() + ' redacted terms'">{{ redactionCount() }}<span class="meta-redact-word"> redacted</span></span><span class="sep">·</span></template>{{ fileKind() }}<template v-if="imageSize"><span class="sep">·</span>{{ imageSize }}</template><template v-if="fileSize(filePath) != null"><span class="sep">·</span>{{ formatSize(fileSize(filePath)) }}</template></span>
|
||||
<div class="file-card-actions">
|
||||
<span class="file-card-state" v-if="type == 'error' && content === 'file_too_big'" title="Files over the size limit can't be previewed or downloaded from this link"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg>Over the size limit</span>
|
||||
<div class="tb-file" role="group" aria-label="File actions" v-if="type != 'error'">
|
||||
<a target="_self" class="tb-seg-btn" :title="type == 'IPython' ? 'Open the notebook JSON' : 'Open the raw file'" :href="safeUrl((url))">Raw</a>
|
||||
<a target="_self" class="tb-seg-btn tb-seg-icon" aria-label="Download file" title="Download file" :href="safeUrl((url) + "&download=true")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-download"></use></svg></a>
|
||||
</div>
|
||||
<div class="tb-more">
|
||||
<button type="button" class="btn btn-sm btn-icon-only btn-ghost tb-more-btn" :class="{ 'is-on': moreOpen }" aria-label="More actions" aria-haspopup="true" :aria-expanded="moreOpen ? 'true' : 'false'" @click.stop="moreOpen = !moreOpen"><svg viewBox="0 0 24 24" class="ui-icon" aria-hidden="true" style="fill:currentColor;stroke:none"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg></button>
|
||||
<div class="tb-menu" v-if="moreOpen" role="menu" @click="moreOpen = false">
|
||||
<div class="tb-menu-note" v-if="options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>Snapshot · {{ fmt?.date(options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) }}</div>
|
||||
<div class="tb-menu-sep" v-if="options?.publishedAt || options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate"></div>
|
||||
<a role="menuitem" target="_self" v-if="type != 'error'" :href="safeUrl((url))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg>{{ type == 'IPython' ? 'View notebook JSON' : 'View raw' }}</a>
|
||||
<a role="menuitem" target="_self" v-if="type != 'error'" :href="safeUrl((url) + "&download=true")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-download"></use></svg>Download file</a>
|
||||
<a role="menuitem" target="_self" v-if="options?.download" :href="safeUrl("/api/repo/" + (repoId) + "/zip")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-archive"></use></svg>Download ZIP</a>
|
||||
<button type="button" role="menuitem" @click="copyPath()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg>Copy file path</button>
|
||||
<div class="tb-menu-sep"></div>
|
||||
<a role="menuitem" href="/faq"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-help"></use></svg>FAQ</a>
|
||||
<a role="menuitem" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="media-stage" v-if="type == 'image'"><img class="image-content" :src="safeUrl((url))" :alt="paths[paths.length - 1]" @load="onImageLoad($event)"></div>
|
||||
<div class="media-stage" v-if="type == 'media'"><video class="video-content" controls preload="metadata" playsinline :src="safeUrl((url))"></video></div>
|
||||
<div class="media-stage is-audio" v-if="type == 'audio'"><audio controls preload="metadata" :src="safeUrl((url))"></audio></div>
|
||||
<pdfviewer v-if="type == 'pdf'" :src="safeUrl((url))"></pdfviewer>
|
||||
<div class="notebook-body" v-if="type == 'IPython'"><notebook class="markdown-body" :file="url" :content="content"></notebook></div>
|
||||
<div class="file-state" v-if="type == 'binary'">
|
||||
<svg class="ui-icon file-state-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg>
|
||||
<h2 class="file-state-title">No preview for this file</h2>
|
||||
<p class="file-state-msg">This is a binary file. Download it to open it on your computer.</p>
|
||||
<div class="file-state-actions"><a target="_self" class="btn btn-primary" :href="safeUrl((url) + "&download=true")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-download"></use></svg>Download file</a></div>
|
||||
</div>
|
||||
<div class="file-state" v-if="type == 'error'" role="alert">
|
||||
<svg class="ui-icon file-state-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
<h2 class="file-state-title">{{ content === 'file_too_big' ? 'This file is too large to preview' : 'This file can\'t be shown' }}</h2>
|
||||
<p class="file-state-msg" v-if="content === 'file_too_big'">It is over the size limit for anonymized files, so it can't be previewed or downloaded from this link.</p>
|
||||
<p class="file-state-msg" v-else>{{ fmt.translate('ERRORS.' + content) }}</p>
|
||||
<div class="file-state-actions"><a class="btn btn-primary" :href="safeUrl('/r/' + repoId + '/')">Back to the repository</a><a class="btn btn-outline-ink" href="/faq#formats">Supported files</a></div>
|
||||
</div>
|
||||
<div class="file-state" v-if="content == null && type != 'error'">
|
||||
<svg class="ui-icon file-state-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg>
|
||||
<h2 class="file-state-title">This file is empty</h2>
|
||||
<p class="file-state-msg">It has no content in this snapshot.</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'rate_limited'">
|
||||
<div class="paper-ratelimit-card m-auto" style="max-width:520px;">
|
||||
<div class="paper-ratelimit-head">
|
||||
@@ -123,7 +186,7 @@
|
||||
<p class="paper-error-msg">This repository will be available in <strong>{{ rateLimitCountdown }}</strong>. The page will reload automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'error'">
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'error' && !fileFrame()">
|
||||
<section class="viewer-error paper-error-card file-error-card m-auto" role="alert" aria-labelledby="viewer-error-title">
|
||||
<h1 id="viewer-error-title" class="paper-empty-title">{{ fmt.translate(isSourceAccessError() ? 'VIEWER_ERRORS.access_title' : 'VIEWER_ERRORS.title') }}</h1>
|
||||
<p>{{ fmt.translate('ERRORS.' + content) }}</p>
|
||||
@@ -148,5 +211,4 @@
|
||||
<div class="viewer-loading-lines" aria-hidden="true"><span v-for="line in 10" :key="line"></span></div>
|
||||
</section>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'empty'"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="content == null && !['empty', 'loading', 'error', 'rate_limited'].includes(type)"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'binary'"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" :href="safeUrl((url) + "&download=true")">download it</a>.</h1></div>
|
||||
<div class="file-error container d-flex h-100" v-if="content == null && !['empty', 'loading', 'error', 'rate_limited'].includes(type) && !fileFrame()"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
|
||||
@@ -1,27 +1,27 @@
|
||||
<div class="container paper-page user-settings">
|
||||
<header class="settings-title"><h1>Settings</h1><div class="settings-header-actions"><span>@{{ user?.username }}</span></div></header>
|
||||
<header class="settings-title page-head page-head-main"><h1>Settings</h1><p class="paper-page-lede">Defaults for new anonymizations, hosting and your account.</p></header>
|
||||
<nav class="settings-nav" aria-label="Settings">
|
||||
<button type="button" v-for="tab in [{id:'defaults',label:'Anonymization defaults'},{id:'billing',label:'Hosting & billing'},{id:'account',label:'Account'}]" :aria-current="settingsTab === tab.id ? 'page' : null" @click="settingsTab = tab.id">{{ tab.label }}</button>
|
||||
</nav>
|
||||
<div v-show="settingsTab === 'defaults'">
|
||||
<p class="settings-scope">For new anonymizations only. Existing repositories keep their settings.</p>
|
||||
<form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () => { saveDefault($event) })">
|
||||
<form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @input="unsavedChanges = true" @submit.prevent="submitForm($event, () => { saveDefault($event) })">
|
||||
<section class="settings-row"><div><label class="settings-label" for="terms">Terms to redact</label><p class="settings-help">One term per line.</p></div><div><textarea class="form-control" id="terms" name="terms" :rows="Math.min(14, Math.max(3, String(terms || "").split("\n").length + 1))" placeholder="Jane Smith
|
||||
MIT CSAIL
|
||||
jane.smith@example.org" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "defaultsForm", options: { debounce: 250 } }"></textarea>
|
||||
<details class="settings-syntax"><summary>Regex & replacement syntax</summary><p>Matches become <code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>. Use <code>term=>replacement</code> for a custom replacement. Regex is supported.</p></details>
|
||||
<div class="invalid-feedback" v-show="defaultsForm?.terms?.errors?.format">Terms are in an invalid format.</div>
|
||||
</div></section>
|
||||
<section class="settings-row"><h2>Reviewer display</h2><div><div class="settings-checks"><label class="settings-check"><input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "defaultsForm", options: {} }">Keep links</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "defaultsForm", options: {} }">Render notebooks</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "defaultsForm", options: {} }">Show images</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "defaultsForm", options: {} }">Show PDFs</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value => { options.loc = value }, value: options?.loc, form: "defaultsForm", options: {} }">Show line counts</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "defaultsForm", options: {} }">GitHub Pages</label></div><p class="settings-help">Images and PDFs are not anonymized.</p><p class="settings-help">GitHub Pages must use the anonymized branch.</p></div></section>
|
||||
<section class="settings-row"><h2>Reviewer display</h2><div><div class="settings-checks"><label class="settings-check"><input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "defaultsForm", options: {} }">Keep links</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "defaultsForm", options: {} }">Display images</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "defaultsForm", options: {} }">Display PDFs</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "defaultsForm", options: {} }">Display notebooks</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value => { options.loc = value }, value: options?.loc, form: "defaultsForm", options: {} }">Show line counts</label><label class="settings-check"><input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "defaultsForm", options: {} }">GitHub Pages</label></div><p class="settings-help">Images and PDFs are not anonymized.</p><p class="settings-help">GitHub Pages must use the anonymized branch.</p></div></section>
|
||||
<section class="settings-row"><h2>Source updates</h2><div><label class="settings-check"><input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "defaultsForm", options: {} }">Auto-update from GitHub</label><p class="settings-help">Latest commit, at most once an hour.</p></div></section>
|
||||
<section class="settings-row"><label class="settings-label" for="expiration">On expiration</label><div><select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value => { options.expirationMode = value }, value: options?.expirationMode, form: "defaultsForm", options: {} }">
|
||||
<option value="redirect">Redirect visitors to the GitHub repository</option>
|
||||
<option value="remove" selected>Remove the anonymized repository</option>
|
||||
</select><p class="settings-help">{{ options.expirationMode === 'redirect' ? 'Redirecting reveals the original repository. Choose the date per anonymization.' : 'Set the expiration date for each repository.' }}</p></div></section>
|
||||
<footer class="settings-save">
|
||||
<div class="settings-save-status draft-status" role="status"><span v-if="draftRestored">Your unsaved draft was restored.</span><span v-else-if="unsavedChanges">Unsaved changes{{ draftSaved ? ' · Draft saved in this tab' : '' }}</span><span v-else>{{ message ? 'Defaults saved' : 'All changes saved' }}</span></div>
|
||||
<div class="settings-save-status draft-status" role="status"><span v-if="draftRestored">Your unsaved draft was restored.</span><span v-else-if="unsavedChanges">Unsaved changes{{ draftSaved ? ' · Draft saved in this tab' : '' }}</span><span v-else>{{ message ? 'Defaults saved' : 'No unsaved changes' }}</span></div>
|
||||
<button type="button" class="draft-discard" v-if="draftRestored || unsavedChanges" @click="discardDraft()">Discard changes</button>
|
||||
<button id="save" type="submit" class="btn btn-ink" :disabled="saving">{{ saving ? 'Saving…' : 'Save defaults' }}</button>
|
||||
<button id="save" type="submit" class="btn btn-ink" :class="{ 'is-saving': saving }" :disabled="saving || !(unsavedChanges || draftRestored)">{{ saving ? 'Saving…' : 'Save defaults' }}</button>
|
||||
<div class="settings-save-error" role="alert" v-if="error">{{ error }}</div>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -49,7 +49,7 @@ jane.smith@example.org" v-field="{ state: viewState, set: value => { terms =
|
||||
<p class="settings-notice" v-if="quota?.repository && !quota.repository.unlimited && quota.repository.used >= quota.repository.total">Your free repositories are used up. Add the next one for {{ profileSlotPlan() ? billingPrice(profileSlotPlan()) : 'a one-time payment' }} when you create it, or <a href="/dashboard">remove work you no longer need</a>.</p>
|
||||
<p class="settings-notice" v-if="profileAuthorGrant()?.status === 'past_due'">A subscription payment is overdue. Update your payment details in Manage billing and invoices.</p>
|
||||
<p class="settings-notice" v-if="profilePastSubscription()">Your previous subscription has ended. Never-expiring repositories stay online for a 90-day grace period. <a href="/dashboard">Review expiration dates</a>.</p>
|
||||
<p class="billing-footnote">Coauthored, paid and conference repositories aren't counted here.</p>
|
||||
<p class="billing-footnote">Co-authored, paid and conference repositories aren't counted here.</p>
|
||||
</section>
|
||||
|
||||
<section class="billing-section" v-if="billing?.enabled && !profileAuthorGrant() && (profilePackPlan() || profileAuthorPlans().length)" aria-labelledby="settings-plan-comparison">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<section class="container py-4" style="max-width: 48rem; overflow-wrap: anywhere">
|
||||
<a href="/">Anonymous GitHub</a>
|
||||
<h1 class="mt-3">Approve artifact access</h1>
|
||||
<p>This records your approval as the repository owner or an authorized coauthor.</p>
|
||||
<p>This records your approval as the repository owner or an authorized co-author.</p>
|
||||
<p v-if="!user?.username">Sign in first, then reopen the artifact link from your submission. <a href="/signin">Sign in</a></p>
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<p v-if="saved" role="status">Consent recorded. <span v-if="handoffEnabled">Continue to review to finish linking.</span><span v-else>Artifact linking is not complete. Return to your submission to check its status.</span></p>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<div class="container paper-page auth-page">
|
||||
<a class="auth-back" href="/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-left"></use></svg> Anonymous GitHub</a>
|
||||
<div class="auth-layout">
|
||||
<header class="auth-intro">
|
||||
<div class="paper-eyebrow">Your research, ready for review</div>
|
||||
@@ -13,15 +12,14 @@
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/login?recover=1" v-if="site_options?.GITHUB_OAUTH_ENABLED">Verify with previous GitHub connection</a>
|
||||
<p v-else>Account verification is currently unavailable. Please try again later.</p>
|
||||
</section>
|
||||
<section v-if="site_options?.GITHUB_APP_ENABLED && !accountRecovery">
|
||||
<div class="connection-top"><i class="fab fa-github" aria-hidden="true"></i><span class="connection-badge">Read-only access</span></div>
|
||||
<section class="auth-card" v-if="site_options?.GITHUB_APP_ENABLED && !accountRecovery">
|
||||
<h2>Continue with GitHub</h2>
|
||||
<p>Connect through the GitHub App with read-only repository access.</p>
|
||||
<ul class="auth-permissions">
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> You choose which repositories to share</li>
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> No permission to write to your code</li>
|
||||
</ul>
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/app/login">Sign in with GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></a>
|
||||
<a class="btn-hero auth-continue auth-continue-github" target="_self" href="/github/app/login"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a>
|
||||
<small>Your existing anonymizations and connections stay with your account.</small>
|
||||
</section>
|
||||
<section class="auth-legacy" v-if="!site_options?.GITHUB_APP_ENABLED && site_options?.GITHUB_OAUTH_ENABLED && !accountRecovery">
|
||||
@@ -33,3 +31,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<partial-view name="partials/footer.htm" :state="viewState"></partial-view>
|
||||
+27
-24
@@ -1,13 +1,15 @@
|
||||
<div class="repository-task status-page">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / Repository</div>
|
||||
<h1>Prepare your anonymous review link</h1>
|
||||
<p class="task-lede">{{ repo?.status === 'ready' ? 'Your repository is ready to share.' : 'Your settings are saved. Track preparation before sharing the link.' }}</p>
|
||||
<ol class="task-trail" aria-label="Repository progress"><li>1. Repository</li><li>2. Review and hosting</li><li aria-current="step">3. Review link</li></ol>
|
||||
<header class="page-head" :class="{ 'task-ready': repo?.status === 'ready' }">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Review link</span></div>
|
||||
<div class="page-head-main"><div>
|
||||
<h1>{{ repo?.status === 'ready' ? 'Your review link is ready' : 'Prepare your anonymous review link' }}</h1>
|
||||
<p class="task-lede">{{ repo?.status === 'ready' ? 'Share this link with reviewers in your submission.' : 'Your settings are saved. Track preparation before sharing the link.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="task-trail flow-trail" aria-label="Repository progress"><li>1. Repository</li><li>2. Review and hosting</li><li aria-current="step">3. Review link</li></ol>
|
||||
</header>
|
||||
<div class="task-columns">
|
||||
<section class="task-preparation" aria-label="Repository preparation">
|
||||
<div class="task-ready" v-if="repo?.status === 'ready'" role="status"><strong>Your review link is ready</strong></div>
|
||||
<h2>{{ repoId }}</h2>
|
||||
<p v-if="repo?.status === 'ready'">Share the link with reviewers. They do not need to sign in.</p>
|
||||
<p v-if="error" class="text-danger" role="alert">Unable to load repository status. <button type="button" class="task-text-button" @click="getStatus()">Try again</button></p>
|
||||
|
||||
<p class="paper-section-copy" v-if="repo?.status !== 'ready'">
|
||||
@@ -51,37 +53,38 @@
|
||||
<li>If you just signed in, the access token may need a moment to propagate — try again.</li>
|
||||
</ul>
|
||||
<div class="paper-error-actions">
|
||||
<a class="btn btn-ink" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit anonymization</a>
|
||||
<a class="btn btn-primary" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit anonymization</a>
|
||||
<a class="btn" href="/faq"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-help"></use></svg> Read the FAQ</a>
|
||||
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Report a bug in Anonymous GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="share-link" v-if="repo?.status == 'ready'">
|
||||
<p class="paper-section-copy">Reviewers can browse the repository at this address without seeing who you are.</p>
|
||||
<div class="share-link-label" id="share-url-label">Review link</div>
|
||||
<div class="share-link-row">
|
||||
<input id="share-url" class="form-control share-link-input" readonly aria-label="Anonymous link" :value="shareUrl" @focus="$event.target.select()">
|
||||
<button type="button" class="btn btn-ink" @click="copyShareUrl()">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="copied ? '#ui-icon-check' : '#project-icon-copy'"></use></svg>
|
||||
<span aria-live="polite" v-text="copied ? 'Copied' : 'Copy link'"></span>
|
||||
</button>
|
||||
<code id="share-url" class="share-link-url" tabindex="0" role="textbox" aria-readonly="true" aria-labelledby="share-url-label" @focus="selectShareUrl($event)">{{ shareUrl }}</code>
|
||||
<div class="share-link-buttons">
|
||||
<button type="button" class="btn btn-primary share-copy" @click="copyShareUrl()">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="copied ? '#ui-icon-check' : '#project-icon-copy'"></use></svg>
|
||||
<span aria-live="polite" v-text="copied ? 'Copied' : 'Copy link'"></span>
|
||||
</button>
|
||||
<a class="btn share-open" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> Open repository
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="share-link-actions">
|
||||
<a class="btn" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> Open repository
|
||||
</a>
|
||||
<a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl("/w/" + (repoId) + "/")">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> Open GitHub page
|
||||
</a>
|
||||
<p class="share-link-note">{{ repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate ? 'Expires ' + fmt.date(repo.options.expirationDate) : 'Never expires' }}<span aria-hidden="true"> · </span>Reviewers don’t need an account<template v-if="repo?.options?.page"><span aria-hidden="true"> · </span><a target="_self" :href="safeUrl("/w/" + (repoId) + "/")">Open GitHub page</a></template></p>
|
||||
<div class="share-link-next">
|
||||
<h2>Need to change something?</h2>
|
||||
<p>Edit the redacted terms or the expiry at any time. The link stays the same.</p>
|
||||
<p class="share-link-next-actions"><a :href="safeUrl("/anonymize/" + (repoId))">Edit anonymization</a><span aria-hidden="true">·</span><a href="/dashboard">Go to My work</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<aside class="task-summary" aria-label="Repository summary">
|
||||
<h3>{{ repoId }}</h3>
|
||||
<dl><dt>Source</dt><dd>{{ repo?.source?.fullName ? 'https://github.com/' + repo.source.fullName : 'Loading…' }}</dd><dt>Lifetime</dt><dd>{{ repo?.options?.expirationMode === 'never' ? 'Never expire while subscribed' : repo?.options?.expirationDate ? 'Until ' + fmt.date(repo.options.expirationDate) : 'Loading…' }}</dd><dt>Review access</dt><dd>Anonymous and free</dd></dl>
|
||||
<div class="task-summary-foot"><strong>{{ repo?.status === 'ready' ? 'Ready to share' : 'Repository preparation' }}</strong><p>Coauthors do not each need a subscription.</p></div>
|
||||
<dl><dt>Source</dt><dd class="task-source" v-if="repo?.source?.fullName" :title="'https://github.com/' + repo.source.fullName">github.com/<wbr>{{ repo.source.fullName.split('/')[0] }}/<wbr>{{ repo.source.fullName.split('/').slice(1).join('/') }}</dd><dd v-else>Loading…</dd><dt>Lifetime</dt><dd>{{ repo?.options?.expirationMode === 'never' ? 'Never expire while subscribed' : repo?.options?.expirationDate ? 'Until ' + fmt.date(repo.options.expirationDate) : 'Loading…' }}</dd><dt>Review access</dt><dd>Anonymous and free</dd></dl>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
+279
-57
@@ -1,10 +1,41 @@
|
||||
import { reactive } from "vue";
|
||||
import { reactive, onScopeDispose } from "vue";
|
||||
import { createTimers, createListeners } from "./state.js";
|
||||
import { duration, statusMsg } from "./formatters.js";
|
||||
|
||||
// One status filter model for every admin list (and the conference page):
|
||||
// a single-select segmented control. One rule everywhere: "All N" is every
|
||||
// record in the list, removed ones included, and selecting it shows exactly
|
||||
// those N rows. The status chips partition All, so their counts add up to it.
|
||||
// `flags()` returns the boolean status map the list already filters on.
|
||||
export const statusGroup = (status) =>
|
||||
["preparing", "download", "downloading", "downloaded", "queue", "anonymizing"].includes(status) ? "preparing"
|
||||
: status === "expiring" ? "expired"
|
||||
: status === "removing" ? "removed"
|
||||
: status;
|
||||
export function installStatusSegments(state, { flags, keys, count, onChange = () => {} }) {
|
||||
state.segView = () => {
|
||||
const f = flags() || {};
|
||||
if (keys.every((k) => !!f[k])) return "all";
|
||||
const on = keys.filter((k) => !!f[k]);
|
||||
return on.length === 1 ? on[0] : "custom";
|
||||
};
|
||||
state.setSegView = (view) => {
|
||||
const f = flags();
|
||||
if (!f) return;
|
||||
for (const k of keys) f[k] = view === "all" ? true : k === view;
|
||||
onChange();
|
||||
};
|
||||
state.segCount = (view) => view === "all" ? keys.reduce((n, k) => n + count(k), 0) : count(view);
|
||||
// Saved preferences from the old "All hides removed" model come back as a
|
||||
// mixed selection; fold them into All so one chip is always selected.
|
||||
if (flags() && state.segView() === "custom") for (const k of keys) flags()[k] = true;
|
||||
}
|
||||
|
||||
// Page setup functions run inside Vue effect scopes.
|
||||
export const repositoriesAdminController = function (state, http, location) {
|
||||
const timers = createTimers();
|
||||
const listen = createListeners();
|
||||
watchMetaLines(timers, listen);
|
||||
state.Math = Math;
|
||||
state.watch("user.status", () => {
|
||||
if (state.user == null) {
|
||||
@@ -128,7 +159,7 @@ export const repositoriesAdminController = function (state, http, location) {
|
||||
dateTo: "",
|
||||
ready: true,
|
||||
expired: true,
|
||||
removed: false,
|
||||
removed: true,
|
||||
error: true,
|
||||
preparing: true,
|
||||
};
|
||||
@@ -269,6 +300,15 @@ export const repositoriesAdminController = function (state, http, location) {
|
||||
}, () => {});
|
||||
}
|
||||
state.showAllStatuses = () => Object.assign(state.query, { ready: true, expired: true, removed: true, error: true, preparing: true, page: 1 });
|
||||
installStatusSegments(state, {
|
||||
flags: () => state.query,
|
||||
keys: ["ready", "preparing", "error", "expired", "removed"],
|
||||
count: (k) => k === "preparing" ? state.statusCountFor("preparing") + state.statusCountFor("download")
|
||||
: k === "expired" ? state.statusCountFor("expired") + state.statusCountFor("expiring")
|
||||
: k === "removed" ? state.statusCountFor("removed") + state.statusCountFor("removing")
|
||||
: state.statusCountFor(k),
|
||||
onChange: () => { state.query.page = 1; },
|
||||
});
|
||||
getRepositories();
|
||||
getStatusCounts();
|
||||
|
||||
@@ -444,6 +484,7 @@ export const usersAdminController = function (state, http, location) {
|
||||
|
||||
export const userAdminController = function (state, http, location, params) {
|
||||
const timers = createTimers();
|
||||
watchMetaLines(timers, createListeners());
|
||||
state.watch("user.status", () => {
|
||||
if (state.user == null) {
|
||||
location.url("/");
|
||||
@@ -513,11 +554,16 @@ export const userAdminController = function (state, http, location, params) {
|
||||
);
|
||||
|
||||
state.statusCountFor = (s) => {
|
||||
return (state.repositories || []).filter((r) => r.status === s).length;
|
||||
return (state.repositories || []).filter((r) => statusGroup(r.status) === s).length;
|
||||
};
|
||||
installStatusSegments(state, {
|
||||
flags: () => state.filters.status,
|
||||
keys: ["ready", "preparing", "error", "expired", "removed"],
|
||||
count: (k) => state.statusCountFor(k),
|
||||
});
|
||||
|
||||
state.repoFiler = (repo) => {
|
||||
if (state.filters.status[repo.status] == false) return false;
|
||||
if (state.filters.status[statusGroup(repo.status)] == false) return false;
|
||||
|
||||
if (state.search.trim().length == 0) return true;
|
||||
|
||||
@@ -820,7 +866,7 @@ export const conferencesAdminController = function (state, http, location) {
|
||||
dateTo: "",
|
||||
ready: true,
|
||||
expired: true,
|
||||
removed: false,
|
||||
removed: true,
|
||||
error: true,
|
||||
preparing: true,
|
||||
};
|
||||
@@ -887,6 +933,12 @@ export const conferencesAdminController = function (state, http, location) {
|
||||
}
|
||||
);
|
||||
}
|
||||
installStatusSegments(state, {
|
||||
flags: () => state.query,
|
||||
keys: ["ready", "preparing", "error", "expired", "removed"],
|
||||
count: (k) => state.statusCountFor(k),
|
||||
onChange: () => { state.query.page = 1; },
|
||||
});
|
||||
getConferences();
|
||||
|
||||
let timeClear = null;
|
||||
@@ -918,6 +970,11 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
state.range = "1h";
|
||||
state.allStates = ["active", "waiting", "delayed", "failed", "completed"];
|
||||
state.stateFilter = { active: true, waiting: true, delayed: true, failed: true, completed: true };
|
||||
installStatusSegments(state, {
|
||||
flags: () => state.stateFilter,
|
||||
keys: state.allStates,
|
||||
count: (k) => (state.jobs || []).filter((j) => j._state === k).length,
|
||||
});
|
||||
state.query = {
|
||||
search: "",
|
||||
autoRefresh: true,
|
||||
@@ -928,6 +985,8 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
};
|
||||
|
||||
state.jobProgressPct = (job) => {
|
||||
// A completed job is done whatever its last reported progress was (an empty bar beside "Completed" reads as stalled).
|
||||
if (job?._state === "completed") return 100;
|
||||
if (job && job.progress && typeof job.progress === "object" && typeof job.progress.percent === "number") {
|
||||
return Math.max(0, Math.min(100, Math.round(job.progress.percent)));
|
||||
}
|
||||
@@ -937,12 +996,19 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
return null;
|
||||
};
|
||||
|
||||
// The Detail column: the stage an active job reports, or why a failed job failed; a dash otherwise.
|
||||
const JOB_STAGES = { get_files: "Fetching files", download: "Downloading", downloading: "Downloading", anonymize: "Anonymizing", anonymizing: "Anonymizing", ready: "Ready", removing: "Removing", preparing: "Preparing" };
|
||||
state.jobDetail = (job) => {
|
||||
if (job?._state === "failed") return statusMsg(job.failedReason || "") || "Failed";
|
||||
const stage = job?.progress && typeof job.progress === "object" ? job.progress.status : null;
|
||||
if (job?._state === "active" && stage) return JOB_STAGES[stage] || statusMsg(stage);
|
||||
return "—";
|
||||
};
|
||||
|
||||
state.jobDuration = (job) => {
|
||||
if (!job.processedOn) return "-";
|
||||
if (!job.processedOn) return "—";
|
||||
const end = job.finishedOn || Date.now();
|
||||
const ms = end - job.processedOn;
|
||||
if (ms < 1000) return ms + "ms";
|
||||
return (ms / 1000).toFixed(1) + "s";
|
||||
return duration(end - job.processedOn);
|
||||
};
|
||||
|
||||
state.metricsPoints = [];
|
||||
@@ -1084,10 +1150,11 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var rect = canvas.parentElement.getBoundingClientRect();
|
||||
var marginLeft = 44;
|
||||
var marginRight = 50;
|
||||
var marginBottom = 20;
|
||||
var totalW = rect.width - 40;
|
||||
var totalH = 180;
|
||||
var marginRight = 44;
|
||||
var marginBottom = 22;
|
||||
// The plot fills its card, which stretches to the height of the stats panel beside it.
|
||||
var totalW = Math.max(200, Math.round(rect.width));
|
||||
var totalH = Math.max(180, Math.min(420, Math.round(rect.height) || 180));
|
||||
var w = totalW - marginLeft - marginRight;
|
||||
var h = totalH - marginBottom;
|
||||
canvas.width = totalW * dpr;
|
||||
@@ -1103,7 +1170,8 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
var completedFill = isDark ? "rgba(111,191,138,0.12)" : "rgba(47,125,79,0.08)";
|
||||
var failedColor = isDark ? "#F08A82" : "#B42318";
|
||||
var failedFill = isDark ? "rgba(240,138,130,0.08)" : "rgba(180,35,24,0.06)";
|
||||
var execColor = isDark ? "#F5C842" : "#B8860B";
|
||||
// One token drives the time line, its seconds axis and its legend key (style.css: --q-time-series).
|
||||
var execColor = getComputedStyle(document.body).getPropertyValue("--q-time-series").trim() || (isDark ? "#E0A94A" : "#946512");
|
||||
|
||||
var pts = state.metricsPoints || [];
|
||||
if (pts.length === 0) {
|
||||
@@ -1150,7 +1218,7 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
// Grid + left Y-axis labels (jobs/min)
|
||||
ctx.textAlign = "right";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = "10px monospace";
|
||||
ctx.font = "11px Inter, system-ui, sans-serif";
|
||||
left.ticks.forEach(function (v) {
|
||||
var y = toY(v);
|
||||
ctx.strokeStyle = gridColor;
|
||||
@@ -1160,7 +1228,8 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
ctx.lineTo(totalW - marginRight, y);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.fillText(v >= 1000 ? (v / 1000).toFixed(1) + "k" : String(v), marginLeft - 6, y);
|
||||
// The origin is implied by the baseline; a "0" here would collide with the first time label.
|
||||
if (v !== 0) ctx.fillText(v >= 1000 ? (v / 1000).toFixed(1) + "k" : String(v), marginLeft - 6, y);
|
||||
});
|
||||
|
||||
// Right Y-axis labels (ms)
|
||||
@@ -1169,26 +1238,40 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
right.ticks.forEach(function (v) {
|
||||
var y = toYr(v);
|
||||
ctx.fillStyle = execColor;
|
||||
ctx.fillText(v >= 1000 ? (v / 1000).toFixed(1) + "s" : v + "ms", totalW - marginRight + 6, y);
|
||||
if (v !== 0) ctx.fillText(duration(v), totalW - marginRight + 6, y);
|
||||
});
|
||||
}
|
||||
|
||||
// X-axis time labels using actual timestamps
|
||||
var now = Date.now();
|
||||
var xLabelCount = Math.min(6, maxLen);
|
||||
ctx.textAlign = "center";
|
||||
// X-axis time labels: "now" under the newest point, then whole steps back (1, 2, 5, 10, 15, 30 min, 1, 2, 3,
|
||||
// 6, 12 h, 1 d) labelled "<n> ago". Each tick sits on the data point nearest its time, and its label is that
|
||||
// point's real age rounded to the step, so a label never claims a time the line doesn't reach.
|
||||
var tEnd = pts[maxLen - 1].ts, tStart = pts[0].ts;
|
||||
var spanMin = Math.max(0, (tEnd - tStart) / 60000);
|
||||
var niceSteps = [1, 2, 5, 10, 15, 30, 60, 120, 180, 360, 720, 1440, 2880, 10080];
|
||||
var stepMin = niceSteps.find(function (m) { return spanMin / m <= 4; }) || niceSteps[niceSteps.length - 1];
|
||||
var fmtSpan = function (m) { return m < 60 ? m + "m" : m < 1440 ? (m / 60) + "h" : (m / 1440) + "d"; };
|
||||
var nearestIndex = function (t) {
|
||||
var best = 0;
|
||||
for (var j = 1; j < maxLen; j++) if (Math.abs(pts[j].ts - t) < Math.abs(pts[best].ts - t)) best = j;
|
||||
return best;
|
||||
};
|
||||
var tolMs = Math.max((tEnd - tStart) / Math.max(1, maxLen - 1) / 2, (tEnd - tStart) * 0.03);
|
||||
ctx.textBaseline = "top";
|
||||
for (var i = 0; i < xLabelCount; i++) {
|
||||
var idx = Math.round((i / (xLabelCount - 1)) * (maxLen - 1));
|
||||
var minsAgo = Math.round((now - pts[idx].ts) / 60000);
|
||||
ctx.fillStyle = labelColor;
|
||||
var lastX = Infinity;
|
||||
for (var k = 0; k * stepMin * 60000 <= (tEnd - tStart) + tolMs && k < 8; k++) {
|
||||
var m = k * stepMin;
|
||||
var idx = nearestIndex(tEnd - m * 60000);
|
||||
if (k > 0 && Math.abs(pts[idx].ts - (tEnd - m * 60000)) > tolMs + 1) continue;
|
||||
var x = toX(idx);
|
||||
var lbl;
|
||||
if (minsAgo <= 0) lbl = "now";
|
||||
else if (minsAgo < 60) lbl = minsAgo + "m";
|
||||
else if (minsAgo < 1440) lbl = Math.round(minsAgo / 60) + "h";
|
||||
else lbl = Math.round(minsAgo / 1440) + "d";
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.fillText(lbl, x, h + 4);
|
||||
var label = m === 0 ? "now" : fmtSpan(m) + " ago";
|
||||
var width = ctx.measureText(label).width;
|
||||
var align = k === 0 ? "right" : idx === 0 ? "left" : "center";
|
||||
var labelRight = align === "right" ? x : align === "left" ? x + width : x + width / 2;
|
||||
if (labelRight > lastX - 12) continue;
|
||||
ctx.textAlign = align;
|
||||
ctx.fillText(label, x, h + 6);
|
||||
lastX = align === "right" ? x - width : align === "left" ? x : x - width / 2;
|
||||
}
|
||||
|
||||
function drawArea(data, yFn, fillStyle, strokeStyle) {
|
||||
@@ -1268,7 +1351,7 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
}
|
||||
|
||||
var p = cs.pts[idx];
|
||||
var now = Date.now();
|
||||
var now = cs.pts[cs.pts.length - 1].ts; // same origin as the axis: the series end is "now"
|
||||
var minsAgo = Math.round((now - p.ts) / 60000);
|
||||
var timeLabel;
|
||||
if (minsAgo <= 0) timeLabel = "now";
|
||||
@@ -1284,7 +1367,7 @@ export const queuesAdminController = function (state, http, location, interval,
|
||||
'<div class="q-tip-completed">● completed: ' + p.completed + '/min</div>' +
|
||||
'<div class="q-tip-failed">● failed: ' + p.failed + '/min</div>';
|
||||
if (p.avgMs > 0) {
|
||||
var dur = p.avgMs >= 1000 ? (p.avgMs / 1000).toFixed(1) + "s" : p.avgMs + "ms";
|
||||
var dur = duration(p.avgMs);
|
||||
html += '<div class="q-tip-exec">● avg time: ' + dur + '</div>';
|
||||
}
|
||||
tooltip.innerHTML = html;
|
||||
@@ -1357,13 +1440,13 @@ export const errorsAdminController = function (state, http, location, interval)
|
||||
if (h < 24) return `${h}h ago`;
|
||||
const d = Math.floor(h / 24);
|
||||
if (d < 7) return `${d}d ago`;
|
||||
return new Date(iso).toLocaleDateString();
|
||||
return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||
};
|
||||
state.absTime = (iso) => {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString();
|
||||
return d.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||
};
|
||||
state.absTimeShort = (iso) => {
|
||||
if (!iso) return "";
|
||||
@@ -1818,19 +1901,27 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
if (!row.ms) return "Not timed";
|
||||
for (var i = 0; i <= bounds.length; i++) {
|
||||
count += row["b" + i] || 0;
|
||||
if (count >= Math.ceil(row.count * 0.95)) return i < bounds.length ? "≤ " + bounds[i] + " ms" : "> 10 s";
|
||||
if (count >= Math.ceil(row.count * 0.95)) return i < bounds.length ? "≤ " + duration(bounds[i]) : "> 10\u202Fs";
|
||||
}
|
||||
return "No samples";
|
||||
};
|
||||
// The alert names only the metrics that are actually flagged (> 90%).
|
||||
state.healthAlert = function () {
|
||||
const sys = (state.data && state.data.system) || {};
|
||||
const hot = [["Memory", sys.memPercent], ["Disk", sys.diskPercent]].filter(([, v]) => v > 90);
|
||||
if (!hot.length) return "";
|
||||
const list = hot.map(([n, v]) => n + " is at " + v + "%").join(" and ");
|
||||
return list + ". New anonymizations may fail until " + (hot.length > 1 ? "they drop." : "it drops.");
|
||||
};
|
||||
state.performanceError = null;
|
||||
// 18400 ms reads better as 18.4 s once a value passes one second.
|
||||
state.formatMs = function (ms) {
|
||||
if (ms == null) return "";
|
||||
return ms < 1000 ? ms + " ms" : +(ms / 1000).toFixed(1) + " s";
|
||||
return duration(ms);
|
||||
};
|
||||
state.latencyBound = function (row, field) {
|
||||
if (!row.count) return "No samples";
|
||||
return row[field] == null ? "> 60 s" : "\u2264 " + state.formatMs(row[field]);
|
||||
return row[field] == null ? "> 60\u202Fs" : "\u2264 " + state.formatMs(row[field]);
|
||||
};
|
||||
state.firstByteBound = function (row) {
|
||||
return state.latencyBound({ count: row.firstByteCount, p95UpperMs: row.firstByteP95UpperMs }, "p95UpperMs");
|
||||
@@ -1870,7 +1961,7 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
var i = 0;
|
||||
var v = b;
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
||||
return v.toFixed(i > 0 ? 1 : 0) + " " + units[i];
|
||||
return v.toFixed(i > 0 ? 1 : 0).replace(/\.0$/, "") + " " + units[i];
|
||||
}
|
||||
state.humanBytes = humanBytes;
|
||||
|
||||
@@ -1916,18 +2007,45 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
if (status === "preparing") names.push("download");
|
||||
var sum = 0;
|
||||
names.forEach(function (n) { sum += state.statusCount(n); });
|
||||
return Math.max(0.4, (sum / total) * 100);
|
||||
// A zero count draws no segment; a tiny non-zero count still gets a visible sliver.
|
||||
return sum ? Math.max(0.4, (sum / total) * 100) : 0;
|
||||
};
|
||||
|
||||
// Real errors are 5xx + 4xx; Info entries are logged but never counted as errors.
|
||||
state.realErrors = function () {
|
||||
var sev = state.data && state.data.errors && state.data.errors.severity;
|
||||
if (!sev) return 0;
|
||||
return (sev.error || 0) + (sev.warn || 0);
|
||||
};
|
||||
// Each bar is its share of the real errors.
|
||||
state.errPct = function (key) {
|
||||
if (!state.data || !state.data.errors) return 0;
|
||||
var max = Math.max(
|
||||
state.data.errors.severity.error,
|
||||
state.data.errors.severity.warn,
|
||||
state.data.errors.severity.info,
|
||||
1
|
||||
);
|
||||
return (state.data.errors.severity[key] / max) * 100;
|
||||
var total = state.realErrors();
|
||||
return total ? ((state.data.errors.severity[key] || 0) / total) * 100 : 0;
|
||||
};
|
||||
|
||||
// Hourly real errors (5xx + 4xx) for the last 24 hours, from /api/admin/errors/stats.
|
||||
state.errorHours = function () {
|
||||
var rows = (state.errorStats && state.errorStats.buckets) || [];
|
||||
var max = rows.reduce(function (m, b) { return Math.max(m, (b.error || 0) + (b.warn || 0)); }, 0);
|
||||
return rows.map(function (b) {
|
||||
var n = (b.error || 0) + (b.warn || 0);
|
||||
var h = new Date(b.hour - 3600 * 1000);
|
||||
return {
|
||||
key: b.hour,
|
||||
pct: max && n ? Math.max(6, (n / max) * 100) : 0,
|
||||
errPct: max ? ((b.error || 0) / max) * 100 : 0,
|
||||
warnPct: max ? ((b.warn || 0) / max) * 100 : 0,
|
||||
bad: (b.error || 0) > 0,
|
||||
zero: !n,
|
||||
title: String(h.getHours()).padStart(2, "0") + ":00 · " + (b.error || 0) + " 5xx, " + (b.warn || 0) + " 4xx",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
state.errorHoursPeak = function () {
|
||||
var rows = (state.errorStats && state.errorStats.buckets) || [];
|
||||
return rows.reduce(function (m, b) { return Math.max(m, (b.error || 0) + (b.warn || 0)); }, 0);
|
||||
};
|
||||
|
||||
function computeDailyHistory(history) {
|
||||
@@ -1969,30 +2087,86 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
var historyMaxes = {};
|
||||
state.historyMax = function (field) { return historyMaxes[field] || 0; };
|
||||
var historyMins = {};
|
||||
// Top of the value scale: a round number at or above the peak whose half is an integer
|
||||
// (2 -> 2, 7 -> 8, 22 -> 30, 161 -> 200), so the 0 / mid / max ticks are whole numbers.
|
||||
state.axisMax = function (field) {
|
||||
var max = historyMaxes[field] || 0;
|
||||
if (max <= 2) return 2;
|
||||
if (max <= 10) return Math.ceil(max / 2) * 2;
|
||||
var unit = Math.pow(10, Math.floor(Math.log10(max)));
|
||||
return Math.ceil(max / unit) * unit;
|
||||
};
|
||||
state.historyBarH = function (d, field) {
|
||||
if (!d || !historyMaxes[field]) return 0;
|
||||
return Math.max(1, Math.round((d[field] / historyMaxes[field]) * 140));
|
||||
};
|
||||
// Cumulative totals barely move against a zero baseline, so they are
|
||||
// drawn between the period's minimum and maximum (shown in the legend).
|
||||
state.historyRangeH = function (d, field) {
|
||||
if (!d || historyMaxes[field] == null) return 0;
|
||||
var span = historyMaxes[field] - historyMins[field];
|
||||
if (!span) return 70;
|
||||
return Math.round(8 + ((d[field] - historyMins[field]) / span) * 132);
|
||||
// Zero (or missing) is 0px: the baseline is drawn separately. A non-zero day keeps at least 1px.
|
||||
if (!d[field]) return 0;
|
||||
return Math.max(1, Math.round((d[field] / state.axisMax(field)) * 140));
|
||||
};
|
||||
state.historyRange = function (field) {
|
||||
return { min: historyMins[field], max: historyMaxes[field] };
|
||||
};
|
||||
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
state.historyLabel = function (d) {
|
||||
if (!d || !d.date) return "";
|
||||
var dt = new Date(d.date);
|
||||
return (dt.getUTCMonth() + 1) + "/" + dt.getUTCDate();
|
||||
return MONTHS[dt.getUTCMonth()] + " " + dt.getUTCDate();
|
||||
};
|
||||
state.historyHasMissing = function () {
|
||||
return !!(state.data && state.data.history || []).some(function (d) { return d && d._empty; });
|
||||
};
|
||||
state.firstDataIndex = function () {
|
||||
var rows = (state.data && state.data.history) || [];
|
||||
for (var i = 0; i < rows.length; i++) if (!rows[i]._empty) return i;
|
||||
return rows.length;
|
||||
};
|
||||
state.usersFirst = function () {
|
||||
var rows = (state.data && state.data.history) || [];
|
||||
var i = state.firstDataIndex();
|
||||
return rows[i] ? rows[i].nbUsers || 0 : 0;
|
||||
};
|
||||
state.usersGained = function () {
|
||||
var rows = (state.data && state.data.history) || [];
|
||||
return rows.reduce(function (sum, d) { return sum + (d._empty ? 0 : d.dailyUsers || 0); }, 0);
|
||||
};
|
||||
// A cumulative total under 10 users reads better as a number than as a chart.
|
||||
state.usersTiny = function () { return (historyMaxes.nbUsers || 0) < 10; };
|
||||
// Cumulative users as a step line / area anchored at 0, in a viewBox of (days x 140).
|
||||
// A cumulative series on a 0-based axis is a flat slab; zoom the axis to the 30-day range
|
||||
// (two "nice" steps that contain every value) so the growth is visible.
|
||||
state.userAxis = function () {
|
||||
var rows = ((state.data && state.data.history) || []).filter(function (d) { return d && !d._empty && d.nbUsers != null; });
|
||||
if (!rows.length) return { lo: 0, hi: 1 };
|
||||
var lo = Math.min.apply(null, rows.map(function (d) { return d.nbUsers; }));
|
||||
var hi = Math.max.apply(null, rows.map(function (d) { return d.nbUsers; }));
|
||||
var span = Math.max(hi - lo, 1);
|
||||
var p = Math.pow(10, Math.floor(Math.log10(span / 2)));
|
||||
var steps = [1, 2, 2.5, 5, 10, 20, 25, 50, 100].map(function (m) { return m * p; });
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
var a = Math.floor(lo / steps[i]) * steps[i];
|
||||
if (a + 2 * steps[i] >= hi) return { lo: a, hi: a + 2 * steps[i] };
|
||||
}
|
||||
return { lo: 0, hi: state.axisMax("nbUsers") };
|
||||
};
|
||||
state.userStepPath = function (area) {
|
||||
var rows = (state.data && state.data.history) || [];
|
||||
var ax = state.userAxis();
|
||||
var max = historyMaxes.nbUsers ? ax.hi - ax.lo : 0;
|
||||
var start = state.firstDataIndex();
|
||||
if (!max || start >= rows.length) return "";
|
||||
var y = function (v) { return (140 - (((v || 0) - ax.lo) / max) * 140).toFixed(2); };
|
||||
var d = area ? "M" + start + " 140" : "";
|
||||
for (var i = start; i < rows.length; i++) {
|
||||
var yy = y(rows[i].nbUsers);
|
||||
d += (d ? " L" : "M") + i + " " + yy + " L" + (i + 1) + " " + yy;
|
||||
}
|
||||
if (area) d += " L" + rows.length + " 140 Z";
|
||||
return d;
|
||||
};
|
||||
|
||||
function load() {
|
||||
state.loadPerformance();
|
||||
state.loadCache();
|
||||
http.get("/api/admin/errors/stats").then(function (r) { state.errorStats = r.data; }, function () { state.errorStats = null; });
|
||||
http.get("/api/admin/overview").then(function (r) {
|
||||
r.data.history = computeDailyHistory(r.data.history);
|
||||
r.data.daily = {
|
||||
@@ -2023,3 +2197,51 @@ export const overviewAdminController = function (state, http, location, interval
|
||||
var stop = interval(load, 30000);
|
||||
state.on("dispose", function () { interval.cancel(stop); });
|
||||
};
|
||||
|
||||
// Admin repository meta lines ([data-meta-fit]): one fixed order (source · ref · conference · size · terms). Items
|
||||
// that don't fit on the line are hidden from the end, and a "+N" chip says how many, its tooltip listing them.
|
||||
function fitMetaLine(line) {
|
||||
const chip = line.querySelector(":scope > .meta-more");
|
||||
const items = [...line.querySelectorAll(":scope > .meta-item")];
|
||||
items.forEach((el) => { el.removeAttribute("data-meta-hidden"); });
|
||||
if (chip) { chip.hidden = true; chip.textContent = ""; chip.removeAttribute("title"); chip.removeAttribute("aria-label"); }
|
||||
if (!line.clientWidth) return;
|
||||
// Measure with the source at its natural width, so the tail yields before the repository name does.
|
||||
line.classList.add("is-fitting");
|
||||
if (line.scrollWidth <= line.clientWidth + 1) { line.classList.remove("is-fitting"); return; }
|
||||
const hidden = [];
|
||||
for (let i = items.length - 1; i >= 0 && line.scrollWidth > line.clientWidth + 1; i--) {
|
||||
items[i].setAttribute("data-meta-hidden", "");
|
||||
hidden.unshift(items[i]);
|
||||
if (chip) {
|
||||
const names = hidden.map((el) => el.dataset.metaLabel || el.textContent.trim().replace(/\s+/g, " "));
|
||||
chip.hidden = false;
|
||||
chip.textContent = "+" + hidden.length;
|
||||
chip.title = names.join(" · ");
|
||||
chip.setAttribute("aria-label", hidden.length + " more: " + names.join(", "));
|
||||
}
|
||||
}
|
||||
line.classList.remove("is-fitting");
|
||||
}
|
||||
// Fits every meta line in the document while the calling controller's scope lives: on render changes, resize and
|
||||
// font load. The observer and listener are released with the scope (no work after the view is gone).
|
||||
function watchMetaLines(timers, listen) {
|
||||
if (typeof document === "undefined" || typeof MutationObserver === "undefined" || !document.body) return;
|
||||
let queued = false;
|
||||
let observer = null;
|
||||
const fitAll = () => {
|
||||
queued = false;
|
||||
if (!observer) return;
|
||||
document.querySelectorAll("[data-meta-fit]").forEach(fitMetaLine);
|
||||
observer.takeRecords();
|
||||
};
|
||||
const queue = () => { if (!queued && observer) { queued = true; timers.timeout(fitAll, 30); } };
|
||||
observer = new MutationObserver((records) => {
|
||||
if (records.some((r) => !(r.target.closest && r.target.closest(".meta-more")))) queue();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||
onScopeDispose(() => { observer.disconnect(); observer = null; });
|
||||
listen(window, "resize", queue);
|
||||
if (document.fonts?.ready) document.fonts.ready.then(queue);
|
||||
queue();
|
||||
}
|
||||
+405
-28
@@ -3,6 +3,9 @@ import { reactive, nextTick, markRaw } from "vue";
|
||||
import { loadEditor } from "./lazy-assets.js";
|
||||
import { createTimers, createListeners } from "./state.js";
|
||||
import { formDraft } from "./drafts.js";
|
||||
import { statusGroup } from "./admin.js";
|
||||
import { humanTime, dateEcho, shortDate } from "./formatters.js";
|
||||
import { hangCodeLines } from "./components.js";
|
||||
import { escape as globEscape, unescape as globUnescape } from "minimatch";
|
||||
import { compilePreserveMatcher, dedupePreservePatterns } from "../../src/core/preserve-files";
|
||||
import { termHints, escapeTermLine } from "../../src/core/term-hints";
|
||||
@@ -15,8 +18,17 @@ if (typeof window !== "undefined" && window.document?.addEventListener) {
|
||||
if (!img || img.tagName !== "IMG" || img.dataset.failed || !img.closest?.(".markdown-body, .anonymize-preview-body")) return;
|
||||
img.dataset.failed = "1";
|
||||
const ph = window.document.createElement("span");
|
||||
ph.className = "img-unavailable";
|
||||
ph.textContent = img.alt ? "Image unavailable: " + img.alt : "Image unavailable";
|
||||
// Badges and icons become a compact inline chip; only images that declare a
|
||||
// large size keep a block placeholder so the page layout stays the same.
|
||||
const w = parseInt(img.getAttribute("width"), 10) || 0;
|
||||
const h = parseInt(img.getAttribute("height"), 10) || 0;
|
||||
ph.className = w >= 200 || h >= 120 ? "img-unavailable is-large" : "img-unavailable";
|
||||
// Never show raw mask tokens: the label is fixed, and the alt text (with masks spelled
|
||||
// out as "[redacted term N]") is only a tooltip.
|
||||
ph.textContent = "Image unavailable";
|
||||
ph.title = img.alt ? "Image unavailable: " + img.alt.replace(/\bXXXX-(\d+)\b/g, "[redacted term $1]") : "Image unavailable";
|
||||
ph.setAttribute("role", "img");
|
||||
ph.setAttribute("aria-label", ph.title);
|
||||
img.replaceWith(ph);
|
||||
}, true);
|
||||
}
|
||||
@@ -143,6 +155,12 @@ export const installPrismRedactionSlots = () => {
|
||||
// Same width as the XXXX-n it replaces, so code columns stay aligned.
|
||||
mark.style.setProperty("--slot-ch", m[0].length + "ch");
|
||||
mark.textContent = "";
|
||||
// Keep the full term in the text (hidden): Prism re-highlights from
|
||||
// textContent, and a second pass would otherwise turn "XXXX-1" into "1".
|
||||
const src = mark.ownerDocument.createElement("span");
|
||||
src.className = "redact-src";
|
||||
src.textContent = m[0].slice(0, m[0].length - m[1].length);
|
||||
mark.appendChild(src);
|
||||
const idx = mark.ownerDocument.createElement("i");
|
||||
idx.textContent = m[1];
|
||||
mark.appendChild(idx);
|
||||
@@ -240,22 +258,21 @@ export const mainController = function (state, http, location, timeout) {
|
||||
state.darkMode = function (on) {
|
||||
localStorage.setItem("darkMode", on);
|
||||
state.isDarkMode = on;
|
||||
const darkPrismLink = "/css/prism-okaidia.css";
|
||||
// One syntax palette (style.css "R12.6") colours Prism and Ace in both
|
||||
// themes; prism.css only supplies layout, so dark mode needs no theme file.
|
||||
const lightPrismLink = "/css/prism.css";
|
||||
$("link[href='/css/prism-okaidia.css']").remove();
|
||||
if (on) {
|
||||
$("body").addClass("dark-mode");
|
||||
let link = document.createElement("link");
|
||||
link.href = darkPrismLink;
|
||||
link.rel = "stylesheet";
|
||||
document.head.append(link);
|
||||
$(`link[href='${lightPrismLink}']`).remove();
|
||||
} else {
|
||||
$("body").removeClass("dark-mode");
|
||||
let link = document.createElement("link");
|
||||
link.href = lightPrismLink;
|
||||
link.rel = "stylesheet";
|
||||
document.head.append(link);
|
||||
$(`link[href='${darkPrismLink}']`).remove();
|
||||
if (!document.querySelector(`link[href='${lightPrismLink}']`)) {
|
||||
const link = document.createElement("link");
|
||||
link.href = lightPrismLink;
|
||||
link.rel = "stylesheet";
|
||||
document.head.append(link);
|
||||
}
|
||||
}
|
||||
state.emit("dark-mode", on);
|
||||
};
|
||||
@@ -349,6 +366,36 @@ export const mainController = function (state, http, location, timeout) {
|
||||
export const faqController = function (state, http) {
|
||||
const listen = createListeners();
|
||||
const timers = createTimers();
|
||||
// Search: filter the static answers in place. Every word must appear in the question or its answer.
|
||||
// Matching answers are shown open while searching; categories without a match are hidden.
|
||||
state.faqQuery = "";
|
||||
state.faqResults = 0;
|
||||
const normalize = (text) => (text || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, " ");
|
||||
state.filterFaq = () => {
|
||||
const words = normalize(state.faqQuery).trim().split(" ").filter(Boolean);
|
||||
let count = 0;
|
||||
document.querySelectorAll(".faq-section .panel-group").forEach((group) => {
|
||||
let groupCount = 0;
|
||||
group.querySelectorAll(":scope > .panel").forEach((panel) => {
|
||||
const text = panel.dataset.faqText || (panel.dataset.faqText = normalize(panel.textContent));
|
||||
const match = words.every((w) => text.includes(w));
|
||||
panel.classList.toggle("faq-hit", words.length > 0 && match);
|
||||
panel.hidden = words.length > 0 && !match;
|
||||
if (match) groupCount++;
|
||||
});
|
||||
const hide = words.length > 0 && groupCount === 0;
|
||||
group.hidden = hide;
|
||||
const head = group.previousElementSibling;
|
||||
if (head?.classList.contains("faq-cat")) head.hidden = hide;
|
||||
count += groupCount;
|
||||
});
|
||||
state.faqResults = words.length ? count : 0;
|
||||
};
|
||||
state.clearFaqSearch = () => {
|
||||
state.faqQuery = "";
|
||||
state.filterFaq();
|
||||
document.getElementById("faq-search-input")?.focus();
|
||||
};
|
||||
const revealAnswer = () => {
|
||||
const id = window.location.hash.slice(1);
|
||||
const answer = document.getElementById(id);
|
||||
@@ -715,6 +762,8 @@ export const homeController = function (state, http, location, window, timeout)
|
||||
{ key: "pageViews", total: 0, label: "page views" },
|
||||
{ key: "pullRequests", total: 0, label: "pull requests" },
|
||||
];
|
||||
// True once stats have loaded and every total is zero (fresh or beta deployments).
|
||||
state.statsEmpty = () => !!state.stat && state.cards.every((c) => !c.total);
|
||||
function getStat() {
|
||||
http.get("/api/stat/").then((res) => {
|
||||
state.stat = res.data;
|
||||
@@ -837,6 +886,11 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
state.filters.status.progress = true;
|
||||
}
|
||||
};
|
||||
state.idleText = item => {
|
||||
const t = humanTime(item?.anonymizeDate || item?.lastView) || "";
|
||||
if (/ ago$/.test(t)) return "No activity for " + t.replace(/ ago$/, "");
|
||||
return "No activity since " + t.replace(", " + new Date().getFullYear(), "");
|
||||
};
|
||||
state.projectDetailsId = item => "project-details-" + encodeURIComponent(item._type + ":" + (item._id || item._source || "unknown"));
|
||||
state.showProjectDetails = async item => {
|
||||
state.selectedProject = item;
|
||||
@@ -895,10 +949,10 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
// onto these keys so in-progress and error items can be filtered too.
|
||||
state.statusKeyLabels = {
|
||||
ready: "Ready",
|
||||
progress: "In progress",
|
||||
progress: "Preparing",
|
||||
error: "Error",
|
||||
expired: "Expired",
|
||||
removed: "Removed",
|
||||
expired: "Expiring / expired",
|
||||
removed: "Removing / removed",
|
||||
};
|
||||
const inProgress = ["queue", "download", "downloaded", "preparing", "anonymizing"];
|
||||
function statusKey(status) {
|
||||
@@ -916,6 +970,8 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
const dashboardPrefDefaults = {
|
||||
typeFilter: "all",
|
||||
filters: {
|
||||
// Authors open on their live work; removed links sit behind the
|
||||
// "Removed N" view instead of burying the active projects.
|
||||
status: { ready: true, progress: true, error: true, expired: true, removed: false },
|
||||
},
|
||||
orderBy: "-anonymizeDate",
|
||||
@@ -1020,12 +1076,23 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
// What the Expires column should show.
|
||||
if (item.status === "expired" || item.status === "expiring") {
|
||||
item._expiry = { kind: "expired", date: item.options.expirationDate };
|
||||
// "Nov 3" this year, "Nov 3, 2025" otherwise (shared with conference detail).
|
||||
item._expiry.label = shortDate(item.options.expirationDate) || undefined;
|
||||
} else if (item.status !== "ready") {
|
||||
item._expiry = { kind: "none" };
|
||||
} else if (item.options.expirationMode === "never" || !item.options.expirationDate) {
|
||||
item._expiry = { kind: "never" };
|
||||
} else {
|
||||
item._expiry = { kind: "date", date: item.options.expirationDate };
|
||||
// A live link that expires within a week gets a relative, highlighted "in 21 hours" / "in 3 days".
|
||||
const left = new Date(item.options.expirationDate).getTime() - Date.now();
|
||||
if (left > 0 && left <= 7 * 24 * 3600 * 1000) {
|
||||
const hours = Math.max(1, Math.round(left / 3600000));
|
||||
const days = Math.round(left / 86400000);
|
||||
item._expiry.soon = hours < 48
|
||||
? "in " + hours + (hours === 1 ? " hour" : " hours")
|
||||
: "in " + days + " days";
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -1098,6 +1165,7 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
);
|
||||
});
|
||||
state.items = items;
|
||||
state.removedCount = items.filter(item => item._statusKey === "removed").length;
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
@@ -1131,6 +1199,7 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
state.items = (append ? state.items : []).concat(response.data.items.map(decorateSummary));
|
||||
state.dashboardCursor = response.data.cursor;
|
||||
state.dashboardTotals = response.data;
|
||||
if (!append) updateRemovedCount();
|
||||
}, error => {
|
||||
if (generation !== dashboardGeneration) return;
|
||||
if (error.status === 404) { legacyDashboard = true; return loadLegacy(); }
|
||||
@@ -1165,6 +1234,46 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
Object.keys(state.filters.status).filter((k) => state.filters.status[k] === false).length;
|
||||
state.hasHiddenStatus = () => state.hiddenStatusCount() > 0;
|
||||
|
||||
// ---- Status view (Active | Needs attention | Removed N) -------------
|
||||
// "Active" is every status except removed and is the default; "Removed"
|
||||
// shows only removed links. Any other mix is a custom Status filter.
|
||||
const statusKeys = () => Object.keys(state.statusKeyLabels);
|
||||
state.onlyRemovedShown = () => statusKeys().every(k => (k === "removed") === (state.filters.status[k] !== false));
|
||||
state.isActiveDefault = () => statusKeys().every(k => (k === "removed") === (state.filters.status[k] === false));
|
||||
state.statusView = () => {
|
||||
if (state.projectView === "attention") return "attention";
|
||||
if (state.onlyRemovedShown()) return "removed";
|
||||
if (state.filters.status.removed === false) return "active";
|
||||
return "custom";
|
||||
};
|
||||
state.setStatusView = view => {
|
||||
if (view === "attention") return state.setProjectView("attention");
|
||||
state.projectView = "all";
|
||||
statusKeys().forEach(k => { state.filters.status[k] = view === "removed" ? k === "removed" : k !== "removed"; });
|
||||
};
|
||||
// Status chips for filters the view tabs do not already express.
|
||||
state.statusChipKeys = () => {
|
||||
const view = state.statusView();
|
||||
if (view === "removed") return [];
|
||||
return statusKeys().filter(k => state.filters.status[k] === false && !(k === "removed" && view !== "custom"));
|
||||
};
|
||||
// The summary API reports the account total and the filtered count, so the
|
||||
// removed count follows from them whenever status is the only filter. Under a
|
||||
// search or type filter the last known account-wide count is kept.
|
||||
state.removedCount = null;
|
||||
function updateRemovedCount() {
|
||||
if (legacyDashboard) {
|
||||
state.removedCount = state.items.filter(item => item._statusKey === "removed").length;
|
||||
return;
|
||||
}
|
||||
const totals = state.dashboardTotals;
|
||||
if (!totals || state.projectView !== "all" || state.search.trim() || state.typeFilter !== "all") return;
|
||||
if (state.isActiveDefault()) state.removedCount = Math.max(0, totals.total - totals.filtered);
|
||||
else if (state.onlyRemovedShown()) state.removedCount = totals.filtered;
|
||||
}
|
||||
state.showViewTabs = () => state.statusView() !== "active" || state.attentionCount() > 0 || state.removedCount > 0
|
||||
|| (state.removedCount == null && !!state.dashboardTotals && state.dashboardTotals.total > state.dashboardTotals.filtered);
|
||||
|
||||
state.hasActiveFilters = () =>
|
||||
state.projectView !== "all" ||
|
||||
state.typeFilter !== "all" ||
|
||||
@@ -1172,15 +1281,16 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
Object.keys(state.filters.status).some((k) => state.filters.status[k] === false);
|
||||
|
||||
state.showClearFilters = () => state.search.trim().length > 0
|
||||
|| state.typeFilter !== "all" || state.hiddenStatusCount() > 1;
|
||||
|| state.typeFilter !== "all" || state.statusChipKeys().length > 0;
|
||||
// A plain view (Active, Removed, Needs attention) counts its projects;
|
||||
// a search, type or extra status filter makes it "N shown".
|
||||
state.isPlainView = () => !state.showClearFilters();
|
||||
|
||||
state.clearFilters = () => {
|
||||
state.projectView = "all";
|
||||
const view = state.statusView();
|
||||
state.typeFilter = "all";
|
||||
state.search = "";
|
||||
Object.keys(state.filters.status).forEach((k) => {
|
||||
state.filters.status[k] = true;
|
||||
});
|
||||
state.setStatusView(view === "removed" ? "removed" : "active");
|
||||
};
|
||||
|
||||
function waitRepoToBeReady(repoId, callback, onError, previousStatus) {
|
||||
@@ -1307,7 +1417,7 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
const endpoint = `${apiBaseOf(item._type)}/${item._id}/extend`;
|
||||
http.post(endpoint).then(
|
||||
(res) => {
|
||||
const until = res?.data?.expirationDate ? ` until ${new Date(res.data.expirationDate).toLocaleDateString()}` : "";
|
||||
const until = res?.data?.expirationDate ? ` until ${new Date(res.data.expirationDate).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}` : "";
|
||||
if (item._type === "repo") {
|
||||
waitRepoToBeReady(item._id, () => {
|
||||
toast.title = `${item._id} is extended.`;
|
||||
@@ -1374,6 +1484,16 @@ export const statusController = function (state, http, params) {
|
||||
}
|
||||
};
|
||||
|
||||
// Focusing the link box selects the whole URL, like the read-only field it replaces.
|
||||
state.selectShareUrl = event => {
|
||||
const selection = window.getSelection?.();
|
||||
if (!selection || !event?.target) return;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(event.target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
};
|
||||
|
||||
var countdownTimer = null;
|
||||
let pollTimer = null;
|
||||
let destroyed = false;
|
||||
@@ -1731,6 +1851,35 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.html_readme = "";
|
||||
state.isUpdate = false;
|
||||
|
||||
// Landing: the user's GitHub repositories, so step 1 is a pick, not a blank field.
|
||||
state.userRepos = [];
|
||||
state.userReposStatus = "idle";
|
||||
state.userRepoFilter = "";
|
||||
state.loadUserRepos = (force = false) => {
|
||||
if (state.userReposStatus === "loading") return;
|
||||
state.userReposStatus = "loading";
|
||||
http.get("/api/user/all_repositories", force ? { params: { force: "1" } } : {}).then(
|
||||
(res) => {
|
||||
if (sourceDisposed) return;
|
||||
const rows = Array.isArray(res.data) ? res.data.filter(r => r && r.fullName) : [];
|
||||
state.userRepos = rows.sort((x, y) => x.fullName.localeCompare(y.fullName, undefined, { sensitivity: "base" }));
|
||||
state.userReposStatus = "ready";
|
||||
},
|
||||
() => { if (!sourceDisposed) state.userReposStatus = "error"; }
|
||||
);
|
||||
};
|
||||
state.filteredUserRepos = () => {
|
||||
const q = String(state.userRepoFilter || "").trim().toLowerCase();
|
||||
const rows = state.userRepos || [];
|
||||
return q ? rows.filter(r => r.fullName.toLowerCase().includes(q)) : rows;
|
||||
};
|
||||
state.selectUserRepo = (repo) => {
|
||||
if (!repo?.fullName) return;
|
||||
state.sourceUrl = "https://github.com/" + repo.fullName;
|
||||
return state.urlSelected();
|
||||
};
|
||||
if (!params.repoId && !params.pullRequestId && !params.gistId) Promise.resolve().then(() => { if (!sourceDisposed) state.loadUserRepos(); });
|
||||
|
||||
state.previousConfigurations = [];
|
||||
state.previousConfigurationId = "";
|
||||
let previousRepositories;
|
||||
@@ -1753,6 +1902,49 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
previousRepositories = undefined;
|
||||
}
|
||||
}
|
||||
// Long PR and gist URLs scroll inside the field, so name the resolved
|
||||
// source underneath: "owner/repo · PR #938", "owner · gist 311fc90…2351".
|
||||
state.sourceSummary = () => {
|
||||
const url = String(state.sourceUrl || "");
|
||||
let m = /github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)/i.exec(url);
|
||||
if (m) return m[1] + " · PR #" + m[2];
|
||||
m = /gist\.github\.com\/([^/\s]+)\/([0-9a-f]{8,})/i.exec(url);
|
||||
if (m) return m[1] + " · gist " + (m[2].length > 12 ? m[2].slice(0, 7) + "…" + m[2].slice(-4) : m[2]);
|
||||
return "";
|
||||
};
|
||||
// "What reviewers will see": the anonymous URL being configured, shown in
|
||||
// the preview window's address bar and kept in sync with the ID field.
|
||||
state.previewId = () => state.detectedType === "pr" ? state.pullRequestId : state.detectedType === "gist" ? state.gistId : state.repoId;
|
||||
state.previewPath = () => {
|
||||
const id = state.previewId();
|
||||
if (!id) return "";
|
||||
return (state.detectedType === "pr" ? "/pr/" : state.detectedType === "gist" ? "/gist/" : "/r/") + encodeURIComponent(id) + "/";
|
||||
};
|
||||
state.previewUrl = () => {
|
||||
const path = state.previewPath();
|
||||
return path ? "anonymous.4open.science" + path.replace(/\/$/, "") : "";
|
||||
};
|
||||
// Occurrences of XXXX-n in what the preview currently shows.
|
||||
state.previewRedactions = () => {
|
||||
const count = (text) => typeof text === "string" ? (text.match(REDACTION_RE) || []).length : 0;
|
||||
if (state.detectedType === "repo") return count(String(state.html_readme || "").replace(/<[^>]*>/g, " "));
|
||||
const cache = state.detectedType === "pr" ? _prAnonCache : _gistAnonCache;
|
||||
let n = 0;
|
||||
cache.forEach((value) => { n += count(value); });
|
||||
return n;
|
||||
};
|
||||
// Top-level entries for the preview's miniature file tree, from the same
|
||||
// tree the Unredacted-files picker loads (folders first, like the explorer).
|
||||
state.previewTree = () => {
|
||||
if (!state.preserve.commit || !preserveIndex) return [];
|
||||
const root = preserveIndex.dirs.get("");
|
||||
if (!root) return [];
|
||||
const readme = root.files.find((f) => /^readme(\.|$)/i.test(f));
|
||||
const dirs = root.dirs.slice(0, 9).map((d) => ({ name: d, dir: true }));
|
||||
const files = [...root.files].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })).slice(0, Math.max(3, 14 - dirs.length)).map((f) => ({ name: f, dir: false, readme: f === readme }));
|
||||
if (readme && !files.some((f) => f.readme)) files.push({ name: readme, dir: false, readme: true });
|
||||
return [...dirs, ...files];
|
||||
};
|
||||
state.existingConfigurationUrl = () => {
|
||||
const previous = state.previousConfigurations.find(repo => repo.repoId === state.previousConfigurationId);
|
||||
return previous ? "/anonymize/" + encodeURIComponent(previous.repoId) : null;
|
||||
@@ -2285,6 +2477,11 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.preserve.loading = false;
|
||||
}
|
||||
}
|
||||
// The preview window shows the repository's top-level files once the
|
||||
// commit is known (one tree request, shared with the picker).
|
||||
state.watch("source.commit", (commit) => {
|
||||
if (state.detectedType === "repo" && commit && state.sourceUrl) loadPreserveTree().catch(() => {});
|
||||
});
|
||||
state.openPreserveFiles = () => {
|
||||
// Picks are saved first, so only the leading run of plain paths can be
|
||||
// shown as picks; a plain path after another rule stays a pattern to
|
||||
@@ -2456,7 +2653,11 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
} catch (_) { /* fall through with empty base */ }
|
||||
const html = renderMD(state.anonymize_readme, baseUrl);
|
||||
state.html_readme = html;
|
||||
timeout(Prism.highlightAll, 150);
|
||||
timeout(() => {
|
||||
Prism.highlightAll();
|
||||
// Wrapped README code in the preview continues under a hanging indent.
|
||||
if (typeof hangCodeLines === "function") document.querySelectorAll(".reviewer-preview .anonymize-preview-body").forEach(hangCodeLines);
|
||||
}, 150);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3198,6 +3399,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
const normalized = path || "";
|
||||
state.files = state.files.filter((f) => f.path !== normalized);
|
||||
state.files.push(...res.data);
|
||||
state.loadedFolders = { ...state.loadedFolders, [normalized]: true };
|
||||
fetchFileCounts(normalized, generation);
|
||||
return res.data;
|
||||
}, function (err) {
|
||||
@@ -3452,7 +3654,8 @@ export const exploreController = function (state, http, location, params, html,
|
||||
if (bytes == null) return "";
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
const kb = bytes / 1024;
|
||||
return kb < 1024 ? kb.toFixed(1) + " KB" : (kb / 1024).toFixed(1) + " MB";
|
||||
const one = (n) => n.toFixed(1).replace(/\.0$/, "");
|
||||
return kb < 1024 ? one(kb) + " KB" : kb < 1024 * 1024 ? one(kb / 1024) + " MB" : one(kb / 1024 / 1024) + " GB";
|
||||
};
|
||||
state.fileSize = function (path) {
|
||||
if (!path) return null;
|
||||
@@ -3471,7 +3674,28 @@ export const exploreController = function (state, http, location, params, html,
|
||||
link.focus();
|
||||
});
|
||||
};
|
||||
// Notebooks: count the same markers in cell sources and text outputs (not in the JSON keys or image data).
|
||||
let nbCount = { content: null, n: 0 };
|
||||
const notebookRedactions = (content) => {
|
||||
if (nbCount.content === content) return nbCount.n;
|
||||
let nb = content;
|
||||
if (typeof nb === "string") { try { nb = JSON.parse(nb); } catch { nb = null; } }
|
||||
const texts = [];
|
||||
const add = (v) => { if (Array.isArray(v)) texts.push(v.join("")); else if (typeof v === "string") texts.push(v); };
|
||||
((nb && (nb.cells || (nb.worksheets || []).flatMap((w) => w.cells || []))) || []).forEach((cell) => {
|
||||
add(cell?.source ?? cell?.input);
|
||||
(cell?.outputs || []).forEach((o) => {
|
||||
add(o?.text);
|
||||
if (o?.data) { add(o.data["text/plain"]); add(o.data["text/markdown"]); add(o.data["text/html"]); }
|
||||
add(o?.traceback);
|
||||
});
|
||||
});
|
||||
const n = texts.reduce((sum, t) => sum + (t.match(REDACTION_RE) || []).length, 0);
|
||||
nbCount = { content, n };
|
||||
return n;
|
||||
};
|
||||
state.redactionCount = function () {
|
||||
if (state.type === "IPython") return notebookRedactions(state.content);
|
||||
if (typeof state.content !== "string") return 0;
|
||||
const text = state.type === "html" ? state.content.replace(/<[^>]*>/g, " ") : state.content;
|
||||
return (text.match(REDACTION_RE) || []).length;
|
||||
@@ -3529,6 +3753,42 @@ export const exploreController = function (state, http, location, params, html,
|
||||
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
state.activeHeading = id;
|
||||
};
|
||||
// CSV/TSV files up to 1 MB render as a table (sticky header, rules, tabular
|
||||
// figures) with a Raw toggle back to the code view.
|
||||
state.csvView = "table";
|
||||
let csvCache = { content: null, rows: [] };
|
||||
const parseDelimited = (text, sep) => {
|
||||
const rows = [];
|
||||
let row = [], cell = "", quoted = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (quoted) {
|
||||
if (ch === '"') { if (text[i + 1] === '"') { cell += '"'; i++; } else quoted = false; }
|
||||
else cell += ch;
|
||||
} else if (ch === '"' && cell === "") quoted = true;
|
||||
else if (ch === sep) { row.push(cell); cell = ""; }
|
||||
else if (ch === "\n" || ch === "\r") {
|
||||
if (ch === "\r" && text[i + 1] === "\n") i++;
|
||||
row.push(cell); rows.push(row); row = []; cell = "";
|
||||
} else cell += ch;
|
||||
}
|
||||
if (cell !== "" || row.length) { row.push(cell); rows.push(row); }
|
||||
return rows.filter((r) => r.length > 1 || r[0] !== "");
|
||||
};
|
||||
state.isCsv = () => typeof state.content === "string" && state.type !== "html"
|
||||
&& /\.(csv|tsv)$/i.test(state.filePath || "") && state.content.length <= 1024 * 1024;
|
||||
state.csvRows = () => {
|
||||
if (csvCache.content !== state.content) {
|
||||
const sep = /\.tsv$/i.test(state.filePath || "") ? "\t" : ",";
|
||||
csvCache = { content: state.content, rows: parseDelimited(state.content, sep) };
|
||||
}
|
||||
return csvCache.rows;
|
||||
};
|
||||
state.csvNumCol = (ci) => {
|
||||
const rows = state.csvRows().slice(1).map((r) => r[ci]).filter((v) => v != null && String(v).trim() !== "");
|
||||
return rows.length > 0 && rows.every((v) => state.csvNumeric(v));
|
||||
};
|
||||
state.csvNumeric = (value) => /^\s*[-+]?(\d[\d,]*)?(\.\d+)?(e[-+]?\d+)?%?\s*$/i.test(value) && /\d/.test(value);
|
||||
state.redactParts = function (text) {
|
||||
const out = []; let last = 0; let m; const re = /\bXXXX-(\d+)\b/g;
|
||||
text = String(text || "");
|
||||
@@ -3706,6 +3966,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
dirPath ? encodePathForUrl(dirPath) : ""
|
||||
}`;
|
||||
state.showSource = false;
|
||||
state.imageSize = "";
|
||||
// Scripts in a repository's HTML are opt-in, per file — see
|
||||
// html-doc.js. Reset on navigation so trust never carries over from
|
||||
// one file to the next.
|
||||
@@ -3717,7 +3978,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
extension = extension.substring(extensionIndex + 1);
|
||||
}
|
||||
|
||||
const languageNames = { javascript: "JavaScript", typescript: "TypeScript", tsx: "TSX", jsx: "JSX", python: "Python", java: "Java", c_cpp: "C/C++", csharp: "C#", golang: "Go", rust: "Rust", ruby: "Ruby", php: "PHP", html: "HTML", css: "CSS", scss: "SCSS", json: "JSON", yaml: "YAML", markdown: "Markdown", sh: "Shell", dockerfile: "Dockerfile", xml: "XML", sql: "SQL", kotlin: "Kotlin", swift: "Swift", scala: "Scala", r: "R", julia: "Julia", latex: "LaTeX", toml: "TOML", text: "" };
|
||||
const languageNames = { javascript: "JavaScript", typescript: "TypeScript", tsx: "TSX", jsx: "JSX", python: "Python", java: "Java", c_cpp: "C/C++", csharp: "C#", golang: "Go", rust: "Rust", ruby: "Ruby", php: "PHP", html: "HTML", css: "CSS", scss: "SCSS", json: "JSON", yaml: "YAML", markdown: "Markdown", sh: "Shell", dockerfile: "Dockerfile", xml: "XML", sql: "SQL", kotlin: "Kotlin", swift: "Swift", scala: "Scala", r: "R", julia: "Julia", latex: "LaTeX", toml: "TOML", csv: "CSV", tsv: "TSV", text: "" };
|
||||
const detectedMode = getMode(extension);
|
||||
state.languageName = languageNames[detectedMode] ?? (detectedMode ? detectedMode.charAt(0).toUpperCase() + detectedMode.slice(1) : "");
|
||||
state.aceOption = {
|
||||
@@ -3921,10 +4182,45 @@ export const exploreController = function (state, http, location, params, html,
|
||||
state.content = "pdf";
|
||||
return;
|
||||
}
|
||||
if (state.type == "media" || state.type == "audio") {
|
||||
// The player streams (and range-requests) the file itself; reading it
|
||||
// as text first would download a whole video twice.
|
||||
state.content = state.type;
|
||||
return;
|
||||
}
|
||||
|
||||
getContent(state.filePath, state.file);
|
||||
}
|
||||
|
||||
// Files without a text body (media, notebooks, binaries, empty files and
|
||||
// per-file errors) share the code view's framed card and header.
|
||||
const FRAMED_TYPES = ["image", "media", "audio", "pdf", "IPython", "binary"];
|
||||
const FILE_ERRORS = ["file_too_big", "file_not_found", "file_not_accessible", "file_not_supported", "folder_not_supported"];
|
||||
state.fileFrame = () => {
|
||||
if (!state.filePath) return false;
|
||||
if (FRAMED_TYPES.includes(state.type)) return state.content != null;
|
||||
if (state.type === "error") return FILE_ERRORS.includes(state.content);
|
||||
return state.content == null && ["code", "text"].includes(state.type);
|
||||
};
|
||||
state.fileKind = () => {
|
||||
const name = (state.paths || [])[(state.paths || []).length - 1] || "";
|
||||
const dot = name.lastIndexOf(".");
|
||||
const ext = dot > 0 ? name.slice(dot + 1).toUpperCase() : "";
|
||||
if (state.type === "image") return (ext === "JPG" ? "JPEG" : ext) + " image";
|
||||
const MEDIA_NAMES = { WEBM: "WebM", OGG: "Ogg", OGV: "Ogg", MOV: "QuickTime", M4V: "M4V", MKV: "Matroska" };
|
||||
if (state.type === "media") return (MEDIA_NAMES[ext] || ext) + " video";
|
||||
if (state.type === "audio") return ext + " audio";
|
||||
if (state.type === "pdf") return "PDF";
|
||||
if (state.type === "IPython") return "Jupyter notebook";
|
||||
if (state.type === "binary") return "Binary file";
|
||||
return state.languageName || (ext ? ext + " file" : "File");
|
||||
};
|
||||
state.imageSize = "";
|
||||
state.onImageLoad = (event) => {
|
||||
const img = event.target;
|
||||
if (img?.naturalWidth) state.imageSize = `${img.naturalWidth} × ${img.naturalHeight}`;
|
||||
};
|
||||
|
||||
state.isSourceAccessError = () => ["repo_not_found", "token_expired", "github_app_access_required",
|
||||
"github_app_reconnect_required", "repo_saml_enforcement", "repo_access_limited"].includes(state.content);
|
||||
state.retryContent = () => init();
|
||||
@@ -3933,6 +4229,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
contentGeneration++;
|
||||
cancelContentLoad();
|
||||
state.files = [];
|
||||
state.loadedFolders = {};
|
||||
state.content = null;
|
||||
state.fileCounts = null;
|
||||
listingGeneration++;
|
||||
@@ -4181,6 +4478,34 @@ export const newConferenceController = function (state, http, location, params)
|
||||
location.url("/");
|
||||
}
|
||||
|
||||
// Named-month echo under the review-window dates ("Starts Oct 8, 2025 · in 3 weeks").
|
||||
// The partial stays markup-only, so the echo nodes are placed from here.
|
||||
const renderDateEchoes = () => {
|
||||
for (const [id, prefix, key] of [["startDate", "Starts", "startDate"], ["endDate", "Ends", "endDate"]]) {
|
||||
const input = document.getElementById(id);
|
||||
if (!input) continue;
|
||||
let echo = input.parentElement.querySelector(":scope > .date-echo");
|
||||
if (!echo) {
|
||||
echo = document.createElement("p");
|
||||
echo.className = "date-echo";
|
||||
echo.setAttribute("aria-live", "polite");
|
||||
input.insertAdjacentElement("afterend", echo);
|
||||
}
|
||||
const text = dateEcho(state.options?.[key]);
|
||||
echo.textContent = text ? prefix + " " + text : "";
|
||||
echo.hidden = !text;
|
||||
}
|
||||
};
|
||||
if (typeof state.watchGroup === "function") state.watchGroup(["options.startDate", "options.endDate"], () => nextTick(renderDateEchoes));
|
||||
// A short example that fits the field at every width (the label already names it).
|
||||
const shortNameExample = (tries = 20) => {
|
||||
const input = document.querySelector("input#name.form-control");
|
||||
const idInput = document.querySelector("input#conferenceID.form-control");
|
||||
if (idInput) idInput.placeholder = "e.g. ICSE27";
|
||||
if (input) input.placeholder = "e.g. ICSE 2027";
|
||||
else if (tries > 0) setTimeout(() => shortNameExample(tries - 1), 50);
|
||||
};
|
||||
setTimeout(() => shortNameExample(), 0);
|
||||
state.plans = [];
|
||||
state.plansLoaded = false;
|
||||
// With billing on, the review packages (one per venue size) are the only
|
||||
@@ -4200,7 +4525,7 @@ export const newConferenceController = function (state, http, location, params)
|
||||
state.conferenceBytes = value => {
|
||||
const bytes = Number(value) || 0;
|
||||
const exponent = Math.min(4, Math.floor(Math.log2(Math.max(bytes, 1)) / 10));
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KiB", "MiB", "GiB", "TiB"][exponent]}`;
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KB", "MB", "GB", "TB"][exponent]}`;
|
||||
};
|
||||
state.editionMode = false;
|
||||
state.saving = false;
|
||||
@@ -4350,7 +4675,7 @@ export const newConferenceController = function (state, http, location, params)
|
||||
};
|
||||
|
||||
function conferencePlanName(id) {
|
||||
return { free_conference: "Free (no longer offered to new conferences)", conference_workshop: "Workshop package", conference: "Conference package", conference_large: "Large conference package" }[id] || id || "Free";
|
||||
return { free_conference: "Free (no longer offered to new conferences)", conference_workshop: "Workshop package", conference: "Conference package", conference_large: "Large conference package", premium_conference: "Premium (legacy)", unlimited_conference: "Unlimited (legacy)" }[id] || id || "Free";
|
||||
}
|
||||
|
||||
export const conferenceController = function (state, http, location, params) {
|
||||
@@ -4368,12 +4693,55 @@ export const conferenceController = function (state, http, location, params) {
|
||||
|
||||
state.search = "";
|
||||
state.filters = {
|
||||
status: { ready: true, expired: false, removed: false },
|
||||
status: { ready: true, preparing: true, error: true, expired: true, removed: false },
|
||||
};
|
||||
state.orderBy = "-anonymizeDate";
|
||||
// Same view tabs as the author dashboard: Current (everything except
|
||||
// removed, the default) | Needs attention (errors) | Removed.
|
||||
const conferenceStatusGroup = statusGroup;
|
||||
const confKeys = ["ready", "preparing", "error", "expired", "removed"];
|
||||
const confViewKeys = { current: k => k !== "removed", attention: k => k === "error", removed: k => k === "removed" };
|
||||
state.confView = () => Object.keys(confViewKeys).find(v => confKeys.every(k => confViewKeys[v](k) === (state.filters.status[k] !== false))) || "custom";
|
||||
state.setConfView = (view) => { confKeys.forEach(k => { state.filters.status[k] = (confViewKeys[view] || confViewKeys.current)(k); }); };
|
||||
state.confCount = (view) => (state.conference?.repositories || []).filter((r) => confViewKeys[view](conferenceStatusGroup(r.status))).length;
|
||||
|
||||
state.copyingId = null;
|
||||
state.copyRepoLink = async (repo) => {
|
||||
if (!repo?.repoId || state.copyingId) return;
|
||||
const link = new URL("/r/" + repo.repoId + "/", window.location.origin).href;
|
||||
state.copyingId = repo.repoId;
|
||||
try {
|
||||
await copyText(link);
|
||||
state.addToast({ title: "Link copied", body: "The anonymous link is ready to share." });
|
||||
} catch {
|
||||
state.addToast({ title: "Copy this anonymous link", body: link });
|
||||
} finally { state.copyingId = null; }
|
||||
};
|
||||
|
||||
// Expired rows carry the same "Extend 6 months" remedy as My work. The API lets the repository's authors
|
||||
// and admins extend; anyone else gets an explanation instead of a silent failure.
|
||||
state.extendingId = null;
|
||||
state.extendRepo = (repo) => {
|
||||
if (!repo?.repoId || state.extendingId) return;
|
||||
state.extendingId = repo.repoId;
|
||||
http.post("/api/repo/" + repo.repoId + "/extend").then(
|
||||
(res) => {
|
||||
const until = res?.data?.expirationDate ? ` until ${new Date(res.data.expirationDate).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}` : "";
|
||||
state.addToast({ title: `${repo.repoId} is extended.`, body: `Reviewers can open it again${until}.` });
|
||||
repo.status = "ready";
|
||||
if (res?.data?.expirationDate) repo.options = { ...(repo.options || {}), expirationDate: res.data.expirationDate };
|
||||
state.extendingId = null;
|
||||
},
|
||||
(error) => {
|
||||
const code = error?.status || error?.data?.status;
|
||||
state.addToast({ title: `Couldn't extend ${repo.repoId}.`, body: code === 401 || code === 403 ? "Only the repository's authors can extend this link. Ask them to extend it from My work." : "Try again in a moment." });
|
||||
state.extendingId = null;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
state.repoFiler = (repo) => {
|
||||
if (state.filters.status[repo.status] == false) return false;
|
||||
if (state.filters.status[conferenceStatusGroup(repo.status)] == false) return false;
|
||||
|
||||
const query = state.search.trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
@@ -4444,3 +4812,12 @@ export const signinController = function (state, http) {
|
||||
state.accountRecovery = false;
|
||||
http.get("/github/account-recovery").then(res => { state.accountRecovery = res.data.required; }).catch(() => {});
|
||||
};
|
||||
|
||||
// Repository picker: the bottom fade means "more below", so it goes away once the list is scrolled to the end.
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("scroll", event => {
|
||||
const list = event.target;
|
||||
if (!list || !list.classList || !list.classList.contains("repo-picker-list")) return;
|
||||
list.classList.toggle("is-end", list.scrollTop + list.clientHeight >= list.scrollHeight - 2);
|
||||
}, true);
|
||||
}
|
||||
@@ -47,9 +47,9 @@ export function setupBilling(state, http, onRefresh = () => {}, context = {}) {
|
||||
const bytes = Number(value) || 0;
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const exponent = Math.min(4, Math.floor(Math.log2(bytes) / 10));
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KiB", "MiB", "GiB", "TiB"][exponent]}`;
|
||||
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(bytes / 1024 ** exponent)} ${["B", "KB", "MB", "GB", "TB"][exponent]}`;
|
||||
};
|
||||
state.billingDate = value => value ? new Date(value).toLocaleDateString() : "";
|
||||
state.billingDate = value => value ? new Date(value).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : "";
|
||||
state.loadBilling = () => { state.billingError = ""; state.billingLoading = true; return readBilling(http).then(res => { state.billing = res.data; state.emit?.("billing:updated", res.data); })
|
||||
.catch(() => { state.billingError = "Billing could not be loaded. Try again."; })
|
||||
.finally(() => { state.billingLoading = false; }); };
|
||||
|
||||
@@ -2,6 +2,7 @@ import { loadLibrary, loadEditor } from "./lazy-assets.js";
|
||||
import { h, ref, watch, onMounted, onBeforeUnmount, nextTick, defineAsyncComponent } from "vue";
|
||||
import HtmlDoc from "./html-doc.js";
|
||||
import PdfViewer from "./pdf-viewer.js";
|
||||
import { slotRedactions } from "./app.js";
|
||||
import { render as quotaUsageTemplate } from "../partials/quotaUsage.htm";
|
||||
|
||||
const QuotaUsage = {
|
||||
@@ -15,12 +16,49 @@ const QuotaUsage = {
|
||||
|
||||
const editorModes = new Set(__ACE_MODES__);
|
||||
|
||||
// ```diff fences: tint added, removed and hunk lines like the pull-request diff (Prism has no diff grammar here).
|
||||
export const tintDiffBlocks = (root) => root?.querySelectorAll("pre code.language-diff, pre code.lang-diff").forEach((code) => {
|
||||
if (code.querySelector(".md-diff-line")) return;
|
||||
const lines = code.innerHTML.replace(/\n$/, "").split("\n");
|
||||
code.innerHTML = lines.map((line) => {
|
||||
const c = line.replace(/^(<[^>]+>)+/, "").charAt(0);
|
||||
const kind = line.startsWith("@@") ? "hunk" : c === "+" ? "add" : c === "-" ? "remove" : "";
|
||||
return `<span class="md-diff-line${kind ? " is-" + kind : ""}">${line || " "}</span>`;
|
||||
}).join("\n");
|
||||
});
|
||||
// Reviewer preview only: every code line becomes its own box, so a soft-wrapped line
|
||||
// continues under a hanging indent instead of looking like a new source line. Open
|
||||
// highlight spans are closed at the end of each line and reopened on the next one.
|
||||
export const hangWidth = (text, skip = 0) => {
|
||||
const lead = (String(text).slice(skip).match(/^[ \t]*/)[0] || "").replace(/\t/g, " ").length;
|
||||
return Math.min(skip + lead + 4, 24);
|
||||
};
|
||||
export const hangCodeLines = (root) => {
|
||||
if (!root?.closest?.(".reviewer-preview")) return;
|
||||
root.querySelectorAll("pre code").forEach((code) => {
|
||||
const diffLines = code.querySelectorAll(".md-diff-line");
|
||||
if (diffLines.length) {
|
||||
diffLines.forEach((line) => { if (!line.classList.contains("is-hunk")) line.style.setProperty("--hang", hangWidth(line.textContent, 1) + "ch"); });
|
||||
return;
|
||||
}
|
||||
if (code.querySelector(".code-line")) return;
|
||||
const open = [];
|
||||
code.innerHTML = code.innerHTML.replace(/\n$/, "").split("\n").map((line) => {
|
||||
const head = open.join("");
|
||||
line.replace(/<\/?span\b[^>]*>/g, (tag) => { if (tag.startsWith("</")) open.pop(); else open.push(tag); return tag; });
|
||||
const text = line.replace(/<[^>]+>/g, "");
|
||||
return `<span class="code-line" style="--hang:${hangWidth(text)}ch">${head}${line || " "}${"</span>".repeat(open.length)}</span>`;
|
||||
}).join("\n");
|
||||
});
|
||||
};
|
||||
const Markdown = {
|
||||
props: ["content", "terms", "options"],
|
||||
setup(props) {
|
||||
const host = ref(null);
|
||||
const render = () => {
|
||||
host.value.innerHTML = renderMD(props.content || "", window.location.pathname + "/../");
|
||||
tintDiffBlocks(host.value);
|
||||
hangCodeLines(host.value);
|
||||
};
|
||||
onMounted(() => { render(); watch(() => [props.content, props.terms, props.options], render, { deep: true }); });
|
||||
return () => h("markdown", { ref: host });
|
||||
@@ -30,7 +68,7 @@ const GistFile = {
|
||||
props: ["file", "terms", "options"],
|
||||
setup(props) {
|
||||
const host = ref(null);
|
||||
const highlight = () => nextTick(() => host.value?.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el)));
|
||||
const highlight = () => nextTick(() => { host.value?.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el)); tintDiffBlocks(host.value); hangCodeLines(host.value); });
|
||||
onMounted(highlight);
|
||||
watch(() => [props.file?.content, props.terms, props.options], highlight, { deep: true });
|
||||
return () => {
|
||||
@@ -45,6 +83,29 @@ const GistFile = {
|
||||
};
|
||||
},
|
||||
};
|
||||
// Notebook input cells keep their source layout and scroll sideways. A cell whose code is wider than
|
||||
// its box gets a right-edge fade (gone once scrolled to the end) so hidden code is discoverable.
|
||||
const markScrollableCode = (root) => root?.querySelectorAll(".nb-input pre").forEach((pre) => {
|
||||
if (pre.parentElement?.classList.contains("nb-code-scroll")) return;
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "nb-code-scroll";
|
||||
pre.replaceWith(wrap);
|
||||
wrap.appendChild(pre);
|
||||
const update = () => {
|
||||
const overflow = pre.scrollWidth - pre.clientWidth > 1;
|
||||
wrap.classList.toggle("is-overflowing", overflow);
|
||||
wrap.classList.toggle("is-at-end", overflow && pre.scrollLeft + pre.clientWidth >= pre.scrollWidth - 2);
|
||||
if (!overflow) return;
|
||||
// Touch browsers hide scrollbars, so the cell draws its own thin position indicator.
|
||||
const track = pre.clientWidth - 16;
|
||||
const thumb = Math.max(24, track * pre.clientWidth / pre.scrollWidth);
|
||||
wrap.style.setProperty("--thumb-w", thumb + "px");
|
||||
wrap.style.setProperty("--thumb-x", (track - thumb) * pre.scrollLeft / (pre.scrollWidth - pre.clientWidth) + "px");
|
||||
};
|
||||
pre.addEventListener("scroll", update, { passive: true });
|
||||
window.ResizeObserver ? new ResizeObserver(update).observe(pre) : update();
|
||||
update();
|
||||
});
|
||||
const Notebook = {
|
||||
props: ["file", "content"],
|
||||
setup(props) {
|
||||
@@ -59,8 +120,10 @@ const Notebook = {
|
||||
const json = props.content ? JSON.parse(props.content) : await fetch(props.file?.download_url || props.file, { signal: request.signal }).then(r => { if (!r.ok) throw Error("Notebook request failed"); return r.json(); });
|
||||
await loadLibrary("notebook");
|
||||
if (current !== generation) return;
|
||||
host.value.innerHTML = DOMPurify.sanitize(nb.parse(json).render());
|
||||
// Outputs and Markdown cells show XXXX-n as the same numbered slot as the rest of the viewer.
|
||||
host.value.innerHTML = slotRedactions(DOMPurify.sanitize(nb.parse(json).render()));
|
||||
host.value.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el));
|
||||
markScrollableCode(host.value);
|
||||
} catch (error) { if (current === generation && error.name !== "AbortError") host.value.textContent = "Unable to render the notebook."; }
|
||||
};
|
||||
onMounted(() => { render(); watch(() => [props.file, props.content], render); });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const humanFileSize = value => window.humanFileSize(value);
|
||||
// One size vocabulary across the app (KB, MB, GB, binary multiples), as in the explorer and plans.
|
||||
export const humanFileSize = value => String(window.humanFileSize(value)).replace(/([KMGTPEZY])iB$/, "$1B").replace(/\.0 ([KMGTPEZY]?B)$/, " $1");
|
||||
export const bigNum = (function () {
|
||||
return function bigNum(v) {
|
||||
const n = Number(v) || 0;
|
||||
@@ -108,6 +109,7 @@ export const statusMsg = (function () {
|
||||
gist_not_found: "Gist not found on GitHub",
|
||||
commit_not_found: "Commit not found on GitHub",
|
||||
repo_too_big: "Repository exceeds the size limit",
|
||||
repository_too_big: "Repository exceeds the size limit",
|
||||
quota_exceeded: "Storage quota exceeded",
|
||||
incomplete_record: "Incomplete record: missing identifier",
|
||||
};
|
||||
@@ -122,6 +124,13 @@ export const statusMsg = (function () {
|
||||
return "Rate limited — retrying in " + (min > 0 ? min + "m " + sec + "s" : sec + "s");
|
||||
}
|
||||
if (codes[msg]) return codes[msg];
|
||||
// "code: server detail" → the plain sentence for the code; the raw text stays in the tooltip.
|
||||
var coded = /^([a-z0-9]+(?:_[a-z0-9]+)+):\s*\S/.exec(msg);
|
||||
if (coded) {
|
||||
if (codes[coded[1]]) return codes[coded[1]];
|
||||
var t = coded[1].replace(/_/g, " ");
|
||||
return t.charAt(0).toUpperCase() + t.slice(1);
|
||||
}
|
||||
if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(msg)) {
|
||||
var s = msg.replace(/_/g, " ");
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
@@ -177,11 +186,11 @@ export const diff = (function (html) {
|
||||
? "+"
|
||||
: line.kind === "remove"
|
||||
? "-"
|
||||
: line.kind === "hunk"
|
||||
? "@"
|
||||
: "") +
|
||||
"</td>" +
|
||||
'<td class="diff-code">' +
|
||||
'<td class="diff-code" style="--hang:' +
|
||||
Math.min(((line.text || "").match(/^[ \t]*/)[0] || "").replace(/\t/g, " ").length + 4, 24) +
|
||||
'ch">' +
|
||||
esc(line.text) +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
@@ -266,6 +275,11 @@ export const diff = (function (html) {
|
||||
export function number(value, digits) {
|
||||
return value == null ? "" : Number(value).toLocaleString(undefined, digits == null ? {} : { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
||||
}
|
||||
// Money everywhere: the locale's currency format, cents only when there are cents.
|
||||
export function money(value, currency = "EUR") {
|
||||
const n = Number(value) || 0;
|
||||
return new Intl.NumberFormat(undefined, { style: "currency", currency, minimumFractionDigits: n % 1 ? 2 : 0, maximumFractionDigits: 2 }).format(n);
|
||||
}
|
||||
export function uppercase(value) { return String(value ?? "").toUpperCase(); }
|
||||
export function limitTo(value, count, start = 0) { return (value || []).slice(start, start + count); }
|
||||
export function filter(values, predicate) {
|
||||
@@ -300,3 +314,50 @@ export function date(value, format) {
|
||||
export function plural(count, forms) {
|
||||
return (forms?.[count] ?? forms?.[count === 1 ? "one" : "other"] ?? "").replace(/\{\}/g, count ?? 0);
|
||||
}
|
||||
// Named-month echo for a native date field ("May 12, 2027 · in 6 months"), so a
|
||||
// numeric mm/dd/yyyy value is never the only representation. "YYYY-MM-DD" is
|
||||
// read as a local calendar date (not UTC midnight) so it never shifts a day.
|
||||
export function dateEcho(value) {
|
||||
if (!value) return "";
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value));
|
||||
const d = m ? new Date(+m[1], +m[2] - 1, +m[3]) : new Date(value);
|
||||
if (isNaN(d)) return "";
|
||||
const label = d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||
const today = new Date(); today.setHours(0, 0, 0, 0);
|
||||
const days = Math.round((d - today) / 86400000);
|
||||
let rel;
|
||||
if (days === 0) rel = "today";
|
||||
else if (days === 1) rel = "tomorrow";
|
||||
else if (days === -1) rel = "yesterday";
|
||||
else {
|
||||
const abs = Math.abs(days);
|
||||
const months = Math.round(abs / 30.44);
|
||||
const unit = abs < 14 ? [abs, "day"] : abs < 60 ? [Math.round(abs / 7), "week"] : months < 18 ? [months, "month"] : [Math.round(abs / 365.25), "year"];
|
||||
const text = unit[0] + " " + unit[1] + (unit[0] === 1 ? "" : "s");
|
||||
rel = days > 0 ? "in " + text : text + " ago";
|
||||
}
|
||||
return label + " · " + rel;
|
||||
}
|
||||
// One duration format for every admin table: "640 ms", "7.8 ms", "12.8 s", "35m 02s", "2h 05m".
|
||||
// At most two decimals, no trailing zeros, a narrow no-break space before the unit.
|
||||
export function duration(ms) {
|
||||
if (ms == null || ms === "" || isNaN(ms)) return "";
|
||||
ms = Number(ms);
|
||||
const U = " ";
|
||||
if (ms < 1000) return +(ms < 10 ? ms.toFixed(2) : ms < 100 ? ms.toFixed(1) : Math.round(ms)) + U + "ms";
|
||||
const s = ms / 1000;
|
||||
if (s < 60) return +(s < 10 ? s.toFixed(2) : s.toFixed(1)) + U + "s";
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const total = Math.round(s);
|
||||
if (total < 3600) return Math.floor(total / 60) + "m " + pad(total % 60) + "s";
|
||||
return Math.floor(total / 3600) + "h " + pad(Math.floor((total % 3600) / 60)) + "m";
|
||||
}
|
||||
// "Nov 3" in the current year, "Nov 3, 2025" otherwise: the one date format for Expired rows.
|
||||
export function shortDate(value) {
|
||||
if (!value) return "";
|
||||
const d = new Date(value);
|
||||
if (isNaN(d)) return "";
|
||||
const opts = { day: "numeric", month: "short" };
|
||||
if (d.getFullYear() !== new Date().getFullYear()) opts.year = "numeric";
|
||||
return d.toLocaleDateString(undefined, opts);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export function mountApplication(target = "#app", options = {}) {
|
||||
return () => [
|
||||
iconsTemplate(root, []),
|
||||
// Repository viewers are a full-height app shell; the explorer carries brand and links itself.
|
||||
isolatedConsent || /^\/(r|repository)\//.test(root.path || "") ? null : h("header", { class: ["app-header", { "is-contained": root.path === "/" || root.path === "/faq" }] }, templates["partials/header.htm"](root, headerCache)),
|
||||
isolatedConsent || /^\/(r|repository)\//.test(root.path || "") ? null : h("header", { class: ["app-header", { "is-contained": root.path === "/" || root.path === "/faq", "is-signin": root.path === "/signin" }] }, templates["partials/header.htm"](root, headerCache)),
|
||||
h("main", { class: "app-view align-items-stretch w-100" }, h(RouterView, null, {
|
||||
default: ({ Component, route }) => Component ? h(Component, { key: route.meta.preserveExplorer ? route.matched[0]?.path : route.path }) : null,
|
||||
})),
|
||||
|
||||
@@ -24,6 +24,8 @@ export default {
|
||||
let scheduled = false;
|
||||
let resizeTimer = null;
|
||||
let zoom = 1;
|
||||
// Page 1's width in PDF points; 100% means one point is 1/72 inch.
|
||||
let baseWidth = 0;
|
||||
let currentPage = 1;
|
||||
// Page the reader asked for, held against layout shifts until
|
||||
// anchorUntil passes.
|
||||
@@ -146,7 +148,11 @@ export default {
|
||||
}
|
||||
prevBtn.disabled = currentPage <= 1;
|
||||
nextBtn.disabled = currentPage >= total;
|
||||
zoomLabel.textContent = Math.round(zoom * 100) + "%";
|
||||
// At the fit zoom the page fills the column, whatever its real
|
||||
// scale, so say "Fit"; other steps show the true size.
|
||||
zoomLabel.textContent = zoom === 1 || !baseWidth
|
||||
? "Fit"
|
||||
: Math.round((pageWidth() / (baseWidth * 96 / 72)) * 100) + "%";
|
||||
zoomOutBtn.disabled = zoomIndex() === 0;
|
||||
zoomInBtn.disabled = zoomIndex() === ZOOM_STEPS.length - 1;
|
||||
}
|
||||
@@ -383,6 +389,7 @@ export default {
|
||||
if (myGeneration !== generation) return;
|
||||
const v = first.getViewport({ scale: 1 });
|
||||
if (v.width && v.height) aspect = v.width / v.height;
|
||||
baseWidth = v.width || 0;
|
||||
|
||||
const width = pageWidth();
|
||||
for (let i = 1; i <= doc.numPages; i++) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function initializeTemplate(name, state) {
|
||||
state.mdFindCase = false;
|
||||
state.accountOpen = false;
|
||||
state.helpOpen = false;
|
||||
state.sidebarWidth = Math.min(520, Math.max(220, Number(localStorage.getItem("sidebarWidth")) || 280));
|
||||
state.sidebarWidth = Math.min(520, Math.max(220, Number(localStorage.getItem("sidebarWidth")) || (window.innerWidth >= 1280 ? 320 : 280)));
|
||||
// Shared with the app bar so the brand column lines up with the file tree.
|
||||
document.documentElement.style.setProperty("--sidebar-w", state.sidebarWidth + "px");
|
||||
state.startSidebarResize = (event) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { render as template22 } from "../partials/pr-dashboard.htm";
|
||||
import { render as template23 } from "../partials/profile.htm";
|
||||
import { render as template24 } from "../partials/pullRequest.htm";
|
||||
import { render as template25 } from "../partials/status.htm";
|
||||
import { render as footerTemplate } from "../partials/footer.htm";
|
||||
export const templates = {
|
||||
"partials/reviewConsent.htm": reviewConsentTemplate,
|
||||
"partials/connections.htm": connectionsTemplate,
|
||||
@@ -56,5 +57,6 @@ export const templates = {
|
||||
"partials/pr-dashboard.htm": template22,
|
||||
"partials/profile.htm": template23,
|
||||
"partials/pullRequest.htm": template24,
|
||||
"partials/status.htm": template25
|
||||
"partials/status.htm": template25,
|
||||
"partials/footer.htm": footerTemplate
|
||||
};
|
||||
+41
-3
@@ -14,6 +14,19 @@ function redactedNodes(text) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Full names with a break opportunity (<wbr>) after ".", "-" and "_", so long
|
||||
// names wrap at a separator rather than mid-word. Redaction slots stay whole.
|
||||
function breakableNodes(text) {
|
||||
const parts = redactedNodes(text);
|
||||
const out = [];
|
||||
for (const part of Array.isArray(parts) ? parts : [parts]) {
|
||||
if (typeof part !== "string") { out.push(part); continue; }
|
||||
const pieces = part.split(/(?<=[._-])/);
|
||||
pieces.forEach((piece, i) => { out.push(piece); if (i < pieces.length - 1) out.push(h("wbr")); });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// File sizes from the API are bytes; the global humanFileSize expects bits.
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
@@ -103,14 +116,39 @@ export default {
|
||||
let node = original, name = node.name;
|
||||
const originalPath = (parent + "/" + name).slice(1);
|
||||
if (matching.value && !matching.value.files.has(originalPath) && !matching.value.folders.has(originalPath)) return [];
|
||||
while (node.children?.length === 1) { node = node.children[0]; name += "/" + node.name; }
|
||||
// GitHub-style "a/b" rows join folder chains only, and only once the
|
||||
// folder's listing is loaded: a folder that so far holds just the open
|
||||
// file stays a real, expandable folder in its sorted place.
|
||||
while (node.children?.length === 1 && node.children[0].children && props.page?.loadedFolders?.[(parent + "/" + name).slice(1)]) { node = node.children[0]; name += "/" + node.name; }
|
||||
const path = parent + "/" + name;
|
||||
const folder = !!node.children;
|
||||
const open = matching.value ? opens[path] !== false : !!opens[path];
|
||||
const truncated = folder && props.page?.options?.truncatedFolders?.includes(path.slice(1));
|
||||
const icon = h("span", { class: folder ? "tree-icon-folder" : "far fa-file tree-icon-file", "aria-hidden": "true" },
|
||||
folder ? h("svg", { viewBox: "0 0 14 12", focusable: "false" }, h("path", { d: "M1.5 10.5v-9h4l1.5 2h5.5v7z" })) : null);
|
||||
const label = h("span", { class: "tree-name", title: name.replace(/\bXXXX-(\d+)\b/g, "[redacted term $1]") }, redactedNodes(name));
|
||||
// Names show in full: long ones wrap onto a second line, preferably after
|
||||
// ".", "-" or "_" (a <wbr> after each), with CSS overflow-wrap as the fallback.
|
||||
// Only pathological names (over 80 characters) still truncate in the middle so
|
||||
// the end and extension stay visible; a redaction token is never split.
|
||||
let splitName = false, head = name, tail = "", middle = "";
|
||||
if (name.length > 80) {
|
||||
let tailAt = name.length - 12;
|
||||
for (let i = name.length - 18; i <= name.length - 6; i++) if (i > 0 && "-_. ".includes(name[i])) { tailAt = i; break; }
|
||||
splitName = !/XXXX-\d+/.test(name.slice(Math.max(0, tailAt - 12)));
|
||||
head = name.slice(0, tailAt).trimEnd(); tail = name.slice(tailAt).trimStart();
|
||||
if (splitName) {
|
||||
const budget = Math.max(8, 30 - tail.length - 1);
|
||||
if (head.length > budget && !/XXXX-\d+/.test(head)) {
|
||||
let at = -1;
|
||||
for (let i = Math.min(budget, head.length); i >= 4; i--) if (".-_".includes(head[i])) { at = i; break; }
|
||||
const full = name.slice(0, name.length - tail.length);
|
||||
head = (at > 0 ? head.slice(0, at) : head.slice(0, budget)).trimEnd();
|
||||
middle = full.slice(head.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
const label = h("span", { class: { "tree-name": true, "tree-name-split": splitName, "tree-name-wrap": !splitName }, title: name.replace(/\bXXXX-(\d+)\b/g, "[redacted term $1]") },
|
||||
splitName ? [h("span", { class: { "tree-name-head": true, "is-cut": !!middle } }, redactedNodes(head)), middle ? h("span", { class: "tree-name-middle" }, middle) : null, h("span", { class: "tree-name-tail" }, tail)] : breakableNodes(name));
|
||||
const onClick = event => {
|
||||
focus(event.currentTarget);
|
||||
if (!folder) return;
|
||||
@@ -127,7 +165,7 @@ export default {
|
||||
}, [folder ? h("span", { class: "tree-toggle" }) : h("span", { class: "tree-spacer" }), icon, label,
|
||||
truncated ? h("span", { class: "truncated-warning", title: props.page.fmt.translate("WARNINGS.folder_truncated") }, h("i", { class: "fas fa-exclamation-triangle" })) : null,
|
||||
]);
|
||||
return h("li", { key: path, class: { file: true, folder, open, active: selectedPath() === path, truncated }, title: folder ? "" : `Size: ${formatBytes(node.size || 0)}` }, [link, folder && open ? renderNodes(node.children, path) : null]);
|
||||
return h("li", { key: path, class: { file: true, folder, open, active: selectedPath() === path, truncated }, title: folder ? "" : `Size: ${formatBytes(node.size || 0)}` }, [link, folder && open ? (node.children.length || !props.page?.loadedFolders?.[path.slice(1)] ? renderNodes(node.children, path) : h("ul", h("li", { class: "tree-empty" }, "Empty folder"))) : null]);
|
||||
}));
|
||||
}
|
||||
return () => h("tree", { ref: host, tabindex: 0, onKeydown: keyboard },
|
||||
|
||||
Vendored
+47
-41
@@ -1,44 +1,51 @@
|
||||
"use strict";(()=>{var ya=Object.defineProperty;var ba=e=>{throw TypeError(e)};var ap=(e,t,o)=>t in e?ya(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o;var lp=(e,t)=>{for(var o in t)ya(e,o,{get:t[o],enumerable:!0})};var We=(e,t,o)=>ap(e,typeof t!="symbol"?t+"":t,o),Ui=(e,t,o)=>t.has(e)||ba("Cannot "+o);var se=(e,t,o)=>(Ui(e,t,"read from private field"),o?o.call(e):t.get(e)),Ie=(e,t,o)=>t.has(e)?ba("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o),qe=(e,t,o,i)=>(Ui(e,t,"write to private field"),i?i.call(e,o):t.set(e,o),o),Me=(e,t,o)=>(Ui(e,t,"access private method"),o);var wa=(e,t,o,i)=>({set _(s){qe(e,t,s,o)},get _(){return se(e,t,i)}});var ge=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={markdown:"/script/markdown.6c8dfb7b1d.min.js",notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};function ka(e){let t=e.location.hash.slice(1);if(e.history.replaceState(null,"","/review-link"),t.length>2048)return null;let o=new URLSearchParams(t);if([...o.keys()].length!==3||[...o.keys()].some(a=>!["clientId","intentId","token"].includes(a)))return null;let i=o.get("clientId"),s=o.get("intentId"),r=o.get("token");return!/^[a-f0-9]{32}$/.test(i||"")||!/^[a-f0-9]{32}$/.test(s||"")||!/^[a-f0-9]{64}$/.test(r||"")?null:{contract:"4open.artifacts/1",clientId:i,intentId:s,token:r}}function Ca(e,t){let{http:o,promises:i,window:s}=t,r=t.takeReviewHandoff(),a=r?{clientId:r.clientId,intentId:r.intentId}:null,u,p,v,h,g,m=!0,k,T=i.defer(),w=()=>[...s.crypto.getRandomValues(new Uint8Array(16))].map(q=>q.toString(16).padStart(2,"0")).join("");Object.assign(e,{repositoryId:"",handoffEnabled:!1,busy:!1,preview:null,acceptAccess:!1,acceptRetention:!1,uncertain:!1,saved:!1,expired:!1,error:r?"":"Open a new artifact link from your submission. This link is missing or invalid."});let b=()=>m&&e.user?.username===g;function E(){m=!1,r=u=p=void 0,T.resolve(),s.clearTimeout(k)}e.on("dispose",E),e.on("routeLeave",E),e.watch(()=>e.user?.username,q=>{!g&&q?g=q:g&&q!==g&&(E(),e.preview=null,e.repositoryId="",e.busy=!1,e.saved=!1,e.handoffEnabled=!1,e.error="Your account changed. Open a new artifact link from your submission.")});let N=()=>({timeout:T.promise,headers:{"Content-Type":"application/json","X-Review-CSRF":p}}),O=q=>q?.status===404?"Artifact linking is not available on this server.":q?.status===401?"Sign in, then open a new artifact link from your submission.":q?.status===403?"This account cannot approve this repository or this link.":"The request could not be completed. You can retry from this page.";e.loadPreview=async()=>{if(!m||!r||e.busy||e.uncertain||e.saved||!e.user?.username)return;g=e.user.username;let q=e.repositoryId.trim();if(!/^[A-Za-z0-9_-]{3,128}$/.test(q)){e.error="Enter the anonymous repository ID from its URL.";return}e.busy=!0,e.error="",e.preview=null,u=void 0,e.acceptAccess=e.acceptRetention=!1,e.expired=!1,s.clearTimeout(k),h||(h=w());try{let M=(await o.get("/api/review-consent/csrf",{timeout:T.promise})).data;if(!b())return;if(p=M.csrf,e.handoffEnabled=M.handoffEnabled===!0,!/^[a-f0-9]{64}$/.test(p||""))throw new Error("invalid_csrf");let G=(await o.post("/api/review-consent/preview",{repositoryId:q,intent:{...r,requestId:h}},N())).data;if(!b())return;if(!G||typeof G.ticket!="string"||!G.ticket||G.ticket.length>8192||typeof G.repositoryName!="string"||!["restricted-review","anonymous-link"].iLine truncated
|
||||
`))),e[rn]=ko(s);let r=i||s.props&&s.props.type==="number";Nn(e,t?"change":"input",a=>{a.target.composing||e[rn](Nr(e.value,o,r))}),(o||r)&&Nn(e,"change",()=>{e.value=Nr(e.value,o,r)}),t||(Nn(e,"compositionstart",ph),Nn(e,"compositionend",ld),Nn(e,"change",ld))},mounted(e,{value:t,modifiers:{trim:o,number:i}}){let s=t??"",r=e[pi];delete e[pi],r!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==r?e[rn](Nr(e.value,o,i)):e.value=s},beforeUpdate(e,{value:t,oldValue:o,modifiers:{lazy:i,trim:s,number:r}},a){if(e[rn]=ko(a),e.composing)return;let u=(r||e.type==="number")&&!/^0\d/.test(e.value)?fo(e.value):e.value,p=t??"";if(u===p)return;let v=e.getRootNode();(v instanceof Document||v instanceof ShadowRoot)&&v.activeElement===e&&e.type!=="range"&&(i&&t===o||s&&e.value.trim()===p)||(e.value=p)}},Dr={deep:!0,created(e,t,o){e[rn]=ko(o),Nn(e,"change",()=>{let i=e._modelValue,s=ms(e),r=e.checked,a=e[rn];if(Se(i)){let u=Fs(i,s),p=u!==-1;if(r&&!p)a(i.concat(s));else if(!r&&p){let v=[...i];v.splice(u,1),a(v)}}else if(Ut(i)){let u=new Set(i);r?u.add(s):u.delete(s),a(u)}else a(hd(e,r))})},mounted:dd,beforeUpdate(e,t,o){e[rn]=ko(o),dd(e,t,o)}};function dd(e,{value:t,oldValue:o},i){e._modelValue=t;let s;if(Se(t))s=Fs(t,i.props.value)>-1;else if(Ut(t))s=t.has(i.props.value);else{if(t===o)return;s=Ft(t,hd(e,!0))}e.checked!==s&&(e.checked=s)}var gn={deep:!0,created(e,{value:t,modifiers:{number:o}},i){e._modelValue=t,Nn(e,"change",()=>{let s=Array.prototype.filter.call(e.options,p=>p.selected).map(p=>o?fo(ms(p)):ms(p)),r=e.multiple,a=r?Ut(e._modelValue)?new Set(s):s:s[0],u=e._pendingValue=[r,r?Se(a)?s.slice():s:a];try{e[rn](a)}finally{nt(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[rn]=ko(i)},mounted(e,{value:t}){ud(e,t)},beforeUpdate(e,{value:t},o){e._modelValue=t,e[rn]=ko(o)},updated(e,{value:t}){let o=e._pendingValue;e._pendingValue=void 0,(!o||o[0]!==e.multiple||!fh(t,o[1],o[0]))&&ud(e,t)}};function fh(e,t,o){if(!o)return Ft(e,t);if(Se(e))return Ft(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let i of t)if(!e.has(i))return!1;return!0}return!1}function ud(e,t){let o=e.multiple,i=Se(t);if(!(o&&!i&&!Ut(t))){for(let s=0,r=e.options.length;s<r;s++){let a=e.options[s],u=ms(a);if(o)if(i){let p=typeof u;p==="string"||p==="number"?a.selected=t.some(v=>String(v)===String(u)):a.selected=Fs(t,u)>-1}else a.selected=t.has(u);else if(Ft(ms(a),t)){e.selectedIndex!==s&&(e.selectedIndex=s);return}}!o&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function ms(e){return"_value"in e?e._value:e.value}function hd(e,t){let o=t?"_trueValue":"_falseValue";return o in e?e[o]:t}var hh=["ctrl","shift","alt","meta"],mh={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>hh.some(o=>e[`${o}Key`]&&!t.includes(o))},ce=(e,t)=>{if(!e)return e;let o=e._withMods||(e._withMods={}),i=t.join(".");return o[i]||(o[i]=((s,...r)=>{for(let a=0;a<t.length;a++){let u=mh[t[a]];if(u&&u(s,t))return}return e(s,...r)}))},gh={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},At=(e,t)=>{let o=e._withKeys||(e._withKeys={}),i=t.join(".");return o[i]||(o[i]=(s=>{if(!("key"in s))return;let r=Xt(s.key);if(t.some(a=>a===r||gh[a]===r))return e(s)}))},vh=it({patchProp:dh},Kf),cd;function yh(){return cd||(cd=jl(vh))}var md=((...e)=>{let t=yh().createApp(...e),{mount:o}=t;return t.mount=i=>{let s=wh(i);if(!s)return;let r=t._component;!Te(r)&&!r.render&&!r.template&&(r.template=s.innerHTML),s.nodeType===1&&(s.textContent="");let a=o(s,!1,bh(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),a},t});function bh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function wh(e){return xe(e)?document.querySelector(e):e}var Xn=typeof document<"u";function vd(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function kh(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&vd(e.default)}var je=Object.assign;function hi(e,t){let o={};for(let i in t){let s=t[i];o[i]=Lt(s)?s.map(e):e(s)}return o}var Eo=()=>{},Lt=Array.isArray;function Pr(e,t){let o={};for(let i in e)o[i]=i in t?t[i]:e[i];return o}var yd=/#/g,Ch=/&/g,Eh=/\//g,Sh=/=/g,Nh=/\?/g,bd=/\+/g,Rh=/%5B/g,Dh=/%5D/g,wd=/%5E/g,Ah=/%60/g,kd=/%7B/g,Th=/%7C/g,Cd=/%7D/g,Ph=/%20/g;function _r(e){return e==null?"":encodeURI(""+e).replace(Th,"|").replace(Rh,"[").replace(Dh,"]")}function Ed(e){return _r(e).replace(kd,"{").replace(Cd,"}").replace(wd,"^")}function Ar(e){return _r(e).replace(bd,"%2B").replace(Ph,"+").replace(yd,"%23").replace(Ch,"%26").replace(Ah,"`").replace(kd,"{").replace(Cd,"}").replace(wd,"^")}function _h(e){return Ar(e).replace(Sh,"%3D")}function Oh(e){return _Line truncated
|
||||
"use strict";(()=>{var Aa=Object.defineProperty;var Ta=e=>{throw TypeError(e)};var wp=(e,t,o)=>t in e?Aa(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o;var kp=(e,t)=>{for(var o in t)Aa(e,o,{get:t[o],enumerable:!0})};var Je=(e,t,o)=>wp(e,typeof t!="symbol"?t+"":t,o),Gi=(e,t,o)=>t.has(e)||Ta("Cannot "+o);var re=(e,t,o)=>(Gi(e,t,"read from private field"),o?o.call(e):t.get(e)),Ve=(e,t,o)=>t.has(e)?Ta("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o),Ue=(e,t,o,i)=>(Gi(e,t,"write to private field"),i?i.call(e,o):t.set(e,o),o),qe=(e,t,o)=>(Gi(e,t,"access private method"),o);var Pa=(e,t,o,i)=>({set _(s){Ue(e,t,s,o)},get _(){return re(e,t,i)}});var ve=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ye={markdown:"/script/markdown.6c8dfb7b1d.min.js",notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js",editor:"/script/editor.4f142d3adc.min.js"};function Oa(e){let t=e.location.hash.slice(1);if(e.history.replaceState(null,"","/review-link"),t.length>2048)return null;let o=new URLSearchParams(t);if([...o.keys()].length!==3||[...o.keys()].some(a=>!["clientId","intentId","token"].includes(a)))return null;let i=o.get("clientId"),s=o.get("intentId"),r=o.get("token");return!/^[a-f0-9]{32}$/.test(i||"")||!/^[a-f0-9]{32}$/.test(s||"")||!/^[a-f0-9]{64}$/.test(r||"")?null:{contract:"4open.artifacts/1",clientId:i,intentId:s,token:r}}function Ia(e,t){let{http:o,promises:i,window:s}=t,r=t.takeReviewHandoff(),a=r?{clientId:r.clientId,intentId:r.intentId}:null,u,p,y,m,b,h=!0,w,A=i.defer(),N=()=>[...s.crypto.getRandomValues(new Uint8Array(16))].map(V=>V.toString(16).padStart(2,"0")).join("");Object.assign(e,{repositoryId:"",handoffEnabled:!1,busy:!1,preview:null,acceptAccess:!1,acceptRetention:!1,uncertain:!1,saved:!1,expired:!1,error:r?"":"Open a new artifact link from your submission. This link is missing or invalid."});let R=()=>h&&e.user?.username===b;function v(){h=!1,r=u=p=void 0,A.resolve(),s.clearTimeout(w)}e.on("dispose",v),e.on("routeLeave",v),e.watch(()=>e.user?.username,V=>{!b&&V?b=V:b&&V!==b&&(v(),e.preview=null,e.repositoryId="",e.busy=!1,e.saved=!1,e.handoffEnabled=!1,e.error="Your account changed. Open a new artifact link from your submission.")});let C=()=>({timeout:A.promise,headers:{"Content-Type":"application/json","X-Review-CSRF":p}}),I=V=>V?.status===404?"Artifact linking is not available on this server.":V?.status===401?"Sign in, then open a new artifact link from your submission.":V?.status===403?"This account cannot approve this repository or this link.":"The request could not be completed. You can retry from this page.";e.loadPreview=async()=>{if(!h||!r||e.busy||e.uncertain||e.saved||!e.user?.username)return;b=e.user.username;let V=e.repositoryId.trim();if(!/^[A-Za-z0-9_-]{3,128}$/.test(V)){e.error="Enter the anonymous repository ID from its URL.";return}e.busy=!0,e.error="",e.preview=null,u=void 0,e.acceptAccess=e.acceptRetention=!1,e.expired=!1,s.clearTimeout(w),m||(m=N());try{let q=(await o.get("/api/review-consent/csrf",{timeout:A.promise})).data;if(!R())return;if(p=q.csrf,e.handoffEnabled=q.handoffEnabled===!0,!/^[a-f0-9]{64}$/.test(p||""))throw new Error("invalid_csrf");let W=(await o.post("/api/review-consent/preview",{repositoryId:V,intent:{...r,requestId:m}},C())).data;if(!R())return;if(!W||typeof W.ticket!="string"||!W.ticket||W.ticket.length>8192||typeof W.repositoryName!="string"||!["restricted-review","anonymous-link"].iLine truncated
|
||||
`))),e[rn]=So(s);let r=i||s.props&&s.props.type==="number";Nn(e,t?"change":"input",a=>{a.target.composing||e[rn](Or(e.value,o,r))}),(o||r)&&Nn(e,"change",()=>{e.value=Or(e.value,o,r)}),t||(Nn(e,"compositionstart",Nh),Nn(e,"compositionend",yd),Nn(e,"change",yd))},mounted(e,{value:t,modifiers:{trim:o,number:i}}){let s=t??"",r=e[hi];delete e[hi],r!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==r?e[rn](Or(e.value,o,i)):e.value=s},beforeUpdate(e,{value:t,oldValue:o,modifiers:{lazy:i,trim:s,number:r}},a){if(e[rn]=So(a),e.composing)return;let u=(r||e.type==="number")&&!/^0\d/.test(e.value)?go(e.value):e.value,p=t??"";if(u===p)return;let y=e.getRootNode();(y instanceof Document||y instanceof ShadowRoot)&&y.activeElement===e&&e.type!=="range"&&(i&&t===o||s&&e.value.trim()===p)||(e.value=p)}},_r={deep:!0,created(e,t,o){e[rn]=So(o),Nn(e,"change",()=>{let i=e._modelValue,s=vs(e),r=e.checked,a=e[rn];if(Ne(i)){let u=zs(i,s),p=u!==-1;if(r&&!p)a(i.concat(s));else if(!r&&p){let y=[...i];y.splice(u,1),a(y)}}else if(Ut(i)){let u=new Set(i);r?u.add(s):u.delete(s),a(u)}else a(Sd(e,r))})},mounted:bd,beforeUpdate(e,t,o){e[rn]=So(o),bd(e,t,o)}};function bd(e,{value:t,oldValue:o},i){e._modelValue=t;let s;if(Ne(t))s=zs(t,i.props.value)>-1;else if(Ut(t))s=t.has(i.props.value);else{if(t===o)return;s=Ft(t,Sd(e,!0))}e.checked!==s&&(e.checked=s)}var an={deep:!0,created(e,{value:t,modifiers:{number:o}},i){e._modelValue=t,Nn(e,"change",()=>{let s=Array.prototype.filter.call(e.options,p=>p.selected).map(p=>o?go(vs(p)):vs(p)),r=e.multiple,a=r?Ut(e._modelValue)?new Set(s):s:s[0],u=e._pendingValue=[r,r?Ne(a)?s.slice():s:a];try{e[rn](a)}finally{ot(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[rn]=So(i)},mounted(e,{value:t}){wd(e,t)},beforeUpdate(e,{value:t},o){e._modelValue=t,e[rn]=So(o)},updated(e,{value:t}){let o=e._pendingValue;e._pendingValue=void 0,(!o||o[0]!==e.multiple||!Rh(t,o[1],o[0]))&&wd(e,t)}};function Rh(e,t,o){if(!o)return Ft(e,t);if(Ne(e))return Ft(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let i of t)if(!e.has(i))return!1;return!0}return!1}function wd(e,t){let o=e.multiple,i=Ne(t);if(!(o&&!i&&!Ut(t))){for(let s=0,r=e.options.length;s<r;s++){let a=e.options[s],u=vs(a);if(o)if(i){let p=typeof u;p==="string"||p==="number"?a.selected=t.some(y=>String(y)===String(u)):a.selected=zs(t,u)>-1}else a.selected=t.has(u);else if(Ft(vs(a),t)){e.selectedIndex!==s&&(e.selectedIndex=s);return}}!o&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function vs(e){return"_value"in e?e._value:e.value}function Sd(e,t){let o=t?"_trueValue":"_falseValue";return o in e?e[o]:t}var Dh=["ctrl","shift","alt","meta"],Ah={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Dh.some(o=>e[`${o}Key`]&&!t.includes(o))},ce=(e,t)=>{if(!e)return e;let o=e._withMods||(e._withMods={}),i=t.join(".");return o[i]||(o[i]=((s,...r)=>{for(let a=0;a<t.length;a++){let u=Ah[t[a]];if(u&&u(s,t))return}return e(s,...r)}))},Th={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},mt=(e,t)=>{let o=e._withKeys||(e._withKeys={}),i=t.join(".");return o[i]||(o[i]=(s=>{if(!("key"in s))return;let r=Xt(s.key);if(t.some(a=>a===r||Th[a]===r))return e(s)}))},Ph=lt({patchProp:Ch},rh),kd;function Oh(){return kd||(kd=ed(Ph))}var Nd=((...e)=>{let t=Oh().createApp(...e),{mount:o}=t;return t.mount=i=>{let s=_h(i);if(!s)return;let r=t._component;!Ie(r)&&!r.render&&!r.template&&(r.template=s.innerHTML),s.nodeType===1&&(s.textContent="");let a=o(s,!1,Ih(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),a},t});function Ih(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function _h(e){return tt(e)?document.querySelector(e):e}var Jn=typeof document<"u";function Dd(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Mh(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Dd(e.default)}var Ge=Object.assign;function gi(e,t){let o={};for(let i in t){let s=t[i];o[i]=Lt(s)?s.map(e):e(s)}return o}var Ro=()=>{},Lt=Array.isArray;function qr(e,t){let o={};for(let i in e)o[i]=i in t?t[i]:e[i];return o}var Ad=/#/g,Vh=/&/g,qh=/\//g,$h=/=/g,Lh=/\?/g,Td=/\+/g,Uh=/%5B/g,Fh=/%5D/g,Pd=/%5E/g,Bh=/%60/g,Od=/%7B/g,zh=/%7C/g,Id=/%7D/g,Hh=/%20/g;function $r(e){return e==null?"":encodeURI(""+e).replace(zh,"|").replace(Uh,"[").replace(Fh,"]")}function _d(e){return $r(e).replace(Od,"{").replace(Id,"}").replace(Pd,"^")}function Mr(e){return $r(e).replace(Td,"%2B").replace(Hh,"+").replace(Ad,"%23").replace(Vh,"%26").replace(Bh,"`").replace(Od,"{").replace(Id,"}").replace(Pd,"^")}function jh(e){return Mr(e).replace($h,"%3D")}function Gh(e){return $Line truncated
|
||||
${JSON.stringify(e)}${t?`
|
||||
while being at
|
||||
`+JSON.stringify(t):""}`},[ot.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Bh(t)}" via a navigation guard.`},[ot.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[ot.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[ot.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function Yn(e,t){return je(new Error,{type:e,[Od]:!0},t)}function an(e,t){return e instanceof Error&&Od in e&&(t==null||!!(e.type&t))}var Fh=["params","query","hash"];function Bh(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of Fh)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function Id(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let i=0;i<o.length;++i){let s=o[i].replace(bd," "),r=s.indexOf("="),a=Co(r<0?s:s.slice(0,r)),u=r<0?null:Co(s.slice(r+1));if(a in t){let p=t[a];Lt(p)||(p=t[a]=[p]),p.push(u)}else t[a]=u}return t}function qr(e){let t="";for(let o in e){let i=e[o];if(o=_h(o),i==null){i!==void 0&&(t+=(t.length?"&":"")+o);continue}(Lt(i)?i.map(s=>s&&Ar(s)):[i&&Ar(i)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+o,s!=null&&(t+="="+s))})}return t}function Md(e){let t={};for(let o in e){let i=e[o];i!==void 0&&(t[o]=Lt(i)?i.map(s=>s==null?null:""+s):i==null?i:""+i)}return t}var Vd=Symbol(""),$r=Symbol(""),yi=Symbol(""),bi=Symbol(""),wi=Symbol("");function So(){let e=[];function t(i){return e.push(i),()=>{let s=e.indexOf(i);s>-1&&e.splice(s,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function vn(e,t,o,i,s,r=a=>a()){let a=i&&(i.enterCallbacks[s]=i.enterCallbacks[s]||[]);return()=>new Promise((u,p)=>{let v=m=>{m===!1?p(Yn(ot.NAVIGATION_ABORTED,{from:o,to:t})):m instanceof Error?p(m):Uh(m)?p(Yn(ot.NAVIGATION_GUARD_REDIRECT,{from:t,to:m})):(a&&i.enterCallbacks[s]===a&&typeof m=="function"&&a.push(m),u())},h=r(()=>e.call(i&&i.instances[s],t,o,v)),g=Promise.resolve(h);e.length<3&&(g=g.then(v)),g.catch(m=>p(m))})}function ki(e,t,o,i,s=r=>r()){let r=[];for(let a of e)for(let u in a.components){let p=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(vd(p)){let v=(p.__vccOpts||p)[t];v&&r.push(vn(v,o,i,a,u,s))}else{let v=p();r.push(()=>v.then(h=>{if(!h)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let g=kh(h)?h.default:h;a.mods[u]=h,a.components[u]=g;let m=(g.__vccOpts||g)[t];return m&&vn(m,o,i,a,u,s)()}))}}return r}function qd(e,t){let o=[],i=[],s=[],r=Math.max(t.matched.length,e.matched.length);for(let a=0;a<r;a++){let u=t.matched[a];u&&(e.matched.find(v=>Kn(v,u))?i.push(u):o.push(u));let p=e.matched[a];p&&(t.matched.find(v=>Kn(v,p))||s.push(p))}return[o,i,s]}var zh=()=>location.protocol+"//"+location.host;function Kd(e,t){let{pathname:o,search:i,hash:s}=t,r=e.indexOf("#");if(r>-1){let a=s.includes(e.slice(r))?e.slice(r).length:1,u=s.slice(a);return u[0]!=="/"&&(u="/"+u),Or(u,"")}return Or(o,e)+i+s}function Hh(e,t,o,i){let s=[],r=[],a=null,u=({state:m})=>{let k=Kd(e,location),T=o.value,w=t.value,b=0;if(m){if(o.value=k,t.value=m,a&&a===T){a=null;return}b=w?m.position-w.position:0}else i(k);s.forEach(E=>{E(o.value,T,{delta:b,type:gi.pop,direction:b?b>0?vi.forward:vi.back:vi.unknown})})};function p(){a=o.value}function v(m){s.push(m);let k=()=>{let T=s.indexOf(m);T>-1&&s.splice(T,1)};return r.push(k),k}function h(){if(document.visibilityState==="hidden"){let{history:m}=window;if(!m.state)return;m.replaceState(je({},m.state,{scroll:gs()}),"")}}function g(){for(let m of r)m();r=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",h),document.removeEventListener("visibilitychange",h)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",h),document.addEventListener("visibilitychange",h),{pauseListeners:p,listen:v,destroy:g}}function $d(e,t,o,i=!1,s=!1){return{back:e,current:t,forward:o,replaced:i,position:window.history.length,scroll:s?gs():null}}function jh(e){let{history:t,location:o}=window,i={value:Kd(e,o)},s={value:t.state};s.value||r(i.value,{back:null,current:i.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function r(p,v,h){let g=e.indexOf("#"),m=g>-1?(o.host&&document.querySelector("base")?e:e.slice(g))+p:zh()+e+p;try{t[h?"replaceState":"pushState"](v,"",m),s.value=v}catch(k){console.error(k),o[h?"replace":"assign"](m)}}function a(p,v){r(p,je({},t.state,$d(s.value.back,p,s.value.forward,!0),v,{position:s.value.position}),!0),i.value=p}function u(p,v){let h=je({},s.value,t.state,{forward:p,scroll:gs()});r(h.current,h,!0),r(p,je({},$d(i.value,p,null),{position:h.position+1},v),!1),i.value=p}return{location:i,state:s,push:u,replace:a}}function Yd(e){e=Dd(e);let t=jh(e),o=Hh(e,t.state,t.location,t.replace);function i(r,a=!0){a||o.pauseListeners(),history.go(r)}let s=je({locatLine truncated
|
||||
`)},Yr=class{constructor(t){We(this,"gen");We(this,"threads",[]);We(this,"match",!1);We(this,"runs",[]);We(this,"ids",[]);this.gen=t}get dead(){return!this.threads.length&&!this.match&&!this.runs.length}},hu=(e,t)=>e.ids.length!==t.ids.length||e.runs.length!==t.runs.length||e.ids.some((o,i)=>o!==t.ids[i])?!1:e.runs.every(o=>t.runs.some(i=>i.neg===o.neg&&hu(i.state,o.state))),au=e=>({has:new Float64Array(e),seen:new Float64Array(e),gen:0}),Zn,_n,Xr,Jr,ws=class{constructor(t,o,i){Ie(this,_n);We(this,"dots");We(this,"dotStart");Ie(this,Zn);this.dots=t,this.dotStart=o,qe(this,Zn,i)}state(){return new Yr(++se(this,Zn).gen)}done(t){return t.ids=t.threads.map(o=>o.id).sort((o,i)=>o-i),t.match&&t.ids.unshift(Kr.id),t}restricted(t,o){return o&&(this.dots||this.dotStart&&t===0)}add(t,o,i,s){switch(o.t){case"split":if(!Me(this,_n,Xr).call(this,t,o))return;for(let a of o.out)this.add(t,a,i,s);return;case"neg":if(!Me(this,_n,Xr).call(this,t,o))return;let r=this.state();return this.add(r,o.sub,i,!1),this.done(r),this.addRun(t,{t:"run",neg:o,state:r},i,s);case"star":if(this.restricted(i,s))return;Me(this,_n,Jr).call(this,t,o)&&this.add(t,o.next,i,s);return;case"match":t.match=!0;return;default:Me(this,_n,Jr).call(this,t,o)}}addRun(t,o,i,s){for(let r of t.runs)if(r.neg===o.neg&&hu(r.state,o.state))return;t.runs.push(o),o.state.match||this.add(t,o.neg.next,i,s)}step(t,o,i,s){let r=this.state();for(let a of t.threads)switch(a.t){case"lit":xr(a,o)&&this.add(r,a.next,i+1,s);break;case"any":this.restricted(i,s)||this.add(r,a.next,i+1,s);break;case"class":!this.restricted(i,s)&&a.re.test(o)&&this.add(r,a.next,i+1,s);break;case"star":this.restricted(i,s)||this.add(r,a,i+1,s)}if(!this.restricted(i,s))for(let a of t.runs){if(a.state.dead){this.addRun(r,a,i+1,s);continue}let u=this.step(a.state,o,i,!1);this.addRun(r,{t:"run",neg:a.neg,state:u},i+1,s)}return this.done(r)}};Zn=new WeakMap,_n=new WeakSet,Xr=function(t,o){let{seen:i}=se(this,Zn);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,!0)},Jr=function(t,o){let{has:i}=se(this,Zn);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,t.threads.push(o),!0)};var mu=e=>{let t=e.ids.join();if(!e.runs.length)return t;let o=e.runs.map(i=>`${i.neg.id}(${mu(i.state)})`);return`${t}|${o.sort().join("|")}`},Bm=1<<18,No,Ro,Rn,xn,Dn,An,Tn,Kt,Zr,Ai,gu,Qr=class{constructor(t,o){Ie(this,Kt);Ie(this,No);Ie(this,Ro,new Map);Ie(this,Rn,0);Ie(this,xn);Ie(this,Dn);Ie(this,An);Ie(this,Tn);qe(this,No,t),qe(this,An,new ws(!1,!1,o)),qe(this,Tn,new ws(!1,!0,o))}start(t){if(t){if(se(this,Dn))return se(this,Dn);let i=se(this,Tn).state();return se(this,Tn).add(i,se(this,No),0,!0),qe(this,Dn,Me(this,Kt,Zr).call(this,se(this,Tn).done(i)))}if(se(this,xn))return se(this,xn);let o=se(this,An).state();return se(this,An).add(o,se(this,No),0,!0),qe(this,xn,Me(this,Kt,Ai).call(this,se(this,An).done(o)))}next(t,o){let i=o.charCodeAt(0),s=i<128?void 0:t.next.get(o);if(s)return s;let r=t===se(this,Dn)?Me(this,Kt,Ai).call(this,se(this,Tn).step(t.state,o,0,!0)):Me(this,Kt,Ai).call(this,se(this,An).step(t.state,o,1,!0));return i<128?(t.ascii||(t.ascii=new Array(128),qe(this,Rn,se(this,Rn)+128)),t.ascii[i]=r):t.next.set(o,r),wa(this,Rn)._++,r}};No=new WeakMap,Ro=new WeakMap,Rn=new WeakMap,xn=new WeakMap,Dn=new WeakMap,An=new WeakMap,Tn=new WeakMap,Kt=new WeakSet,Zr=function(t){return{state:t,match:t.match,dead:t.dead,next:new Map}},Ai=function(t){let o=mu(t),i=se(this,Ro).get(o);if(i)return i;se(this,Rn)>Bm&&Me(this,Kt,gu).call(this);let s=Me(this,Kt,Zr).call(this,t);return se(this,Ro).set(o,s),qe(this,Rn,se(this,Rn)+(o.length+1)),s},gu=function(){se(this,Ro).clear(),qe(this,Rn,0),qe(this,xn,void 0),qe(this,Dn,void 0)};var zm=e=>{let t=[];for(let o=e;o.t!=="match";o=o.next){if(o.t==="split"||o.t==="neg")return;t.push(o)}return t},Hm=(e,t,o,i)=>{let s=0,r=o,a=-1,u=0;for(;r<i;){let p=e[s];if(p?.t==="star"){a=s++,u=r;continue}let v=t[r];if(p&&(p.t==="any"||p.t==="lit"&&xr(p,v)||p.t==="class"&&p.re.test(v))){s++,r++;continue}if(a===-1)return!1;s=a+1,r=++u}for(;e[s]?.t==="star";)s++;return s===e.length},vu=e=>{let t=0,o=0;for(let i of e){let[s,r]=jm(i);t+=s,o+=r}return[t,o]},jm=e=>{if(e.t==="star")return[0,1/0];if(e.t!=="ext")return[1,1];if(e.type==="!")return[0,1/0];let t=e.alts.map(vu),o=Math.min(...t.map(r=>r[0])),i=Math.max(...t.map(r=>r[1])),s=i===0?0:1/0;return e.type==="@"?[o,i]:e.type==="?"?[0,i]:e.type==="*"?[0,s]:[o,s]},lu=(e,t,o)=>{for(let i=0;i<e.length;i++)if(!xr(e[i],t[o+i]))return!1;return!0},Gm=/[/\ud800-\udfff]/,Wm=/[\ud800-\udfff]/,Di=e=>Wm.test(e)?[...e]:e,du=e=>{if(e.includes("/"))throw new Error("can only match a single path portion, got: "+e)},Pn,Do,eo,Ti,to,Cs,Pi,no,Es,Ss,Ns,wn,ln,Ao,To,Rs,_i,Wt,oo,bs,ks=class{constructor(t,o={}){Ie(this,oo);We(this,"pattern");We(this,"options");Ie(this,Pn);Ie(this,Do);Ie(this,eo);Ie(this,Ti);Ie(this,to);Ie(this,Cs,!1);Ie(this,Pi);Ie(this,no);Ie(this,Es,!1);Ie(this,Ss,0);Ie(this,Ns,1/0);Ie(this,wn,[]);Ie(this,ln,[]);Ie(this,Ao);Ie(this,To);Ie(this,Rs,!1);Ie(this,_i);Ie(this,Wt);if(t.includes
Line truncated
|
||||
`).forEach((o,i)=>{let{term:s}=ca(o.trim());if(!s)return;let r=[];for(let a=0;a<s.length;a++){if(s[a]==="\\"){a++;continue}Mu.has(s[a])&&!r.includes(s[a])&&r.push(s[a])}r.length&&t.push({line:i,term:s,symbols:r})}),t}function qu(e,t){let o=String(e||"").split(`
|
||||
`),i=o[t];if(i===void 0)return e;let{term:s}=ca(i.trim());if(!s)return e;let r="";for(let u=0;u<s.length;u++){if(s[u]==="\\"){r+=s.slice(u,u+2),u++;continue}r+=Mu.has(s[u])||s[u]==="."?"\\"+s[u]:s[u]}let a=i.indexOf(s);return o[t]=i.slice(0,a)+r+i.slice(a+s.length),o.join(`
|
||||
`)}var ro={VIEWER_ERRORS:{title:"Content could not be loaded",access_title:"GitHub access needs attention",reader_help:"Reading this repository? You do not need to sign in. Try again after the owner restores access, or let them know this anonymous link is unavailable.",owner_help:"Own this repository? If your GitHub account was suspended or recently recovered, sign in to Anonymous GitHub again and check the GitHub connection and repository permissions in your dashboard. Then retry this page. Keep the existing anonymous URL; do not delete the repository to restore access.",retry:"Try again",dashboard:"Open owner dashboard"},ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"We could not load this content. Try again. If it still fails, contact support with the anonymous URL and the time of the error.",unreachable:"The service could not complete this request. Wait a moment and try again.",request_error:"The connection was interrupted. Check your internet connection and try again.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"Anonymous GitHub cannot access the source repository on GitHub. It may be private, unavailable, or require the owner to reconnect GitHub.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub temporarily blocked the request because we hit its API rate limit. Wait a few minutes and try again. If the problem persists and you contact GitHub Support, include the request ID and timestamp shown in GitHub's response (the 'X-GitHub-Request-Id' header).",rate_limited:"GitHub API rate limit reached. The repository will be available shortly \u2014 please wait a moment and refresh.",repoId_already_used:"The repository ID is already used.",invalid_repoId:"The format of the repository ID is invalid.",unsupported_source:"The repository source type is not supported.",branch_not_specified:"The branch is not specified.",branch_not_found:"The branch of the repository cannot be found.",commit_not_specified:"A commit must be specified.",commit_not_found:"The configured commit no longer exists in the source repository. It may have been force-pushed, rebased, or removed.",repo_renamed:"The source repository appears to have been renamed or moved on GitHub. Please refresh and try again \u2014 the cached name has been updated.",invalid_commit_format:"The commit hash format is invalid. It must be a hexadecimal string.",pull_request_not_found:"The requested pull request could not be found.",pull_request_expired:"This pull request has expired and is no longer available.",pull_request_not_ready:"This pull request is still being prepared. Please t
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,g.options),u.restore()})}p();let v=null;e.saveDefault=h=>{if(h&&h.preventDefault&&h.preventDefault(),e.saving)return;let g=u.checkpoint(),m={terms:e.terms.split(`
|
||||
`).map(k=>k.trim()).filter(k=>k.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",m).then(()=>{u.accept(g),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",v&&i.cancel(v),v=i(()=>{e.message=null},2500)},k=>{e.saving=!1;let T=k&&k.data&&k.data.error,w="Unable to save your defaults. Please try again.";Promise.all([o("ERRORS."+T),o("ERRORS.unknown_error")]).then(([b,E])=>{e.error=T&&b!==E?b:w},()=>{e.error=w})})},e.deleteAccount=()=>{e.deletingAccount||!e.confirmDeleteAccount||e.deleteUsername!==e.user?.username||(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Gu=function(e,t,o){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(i=>{o.url("/dashboard")},i=>{e.error=i.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Wu=function(e,t,o,i,s){e.user&&!e.user.status&&o.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&o.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,p){let v={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(v===void 0)return;u.preventDefault();let h=e.features.length,g=v==="first"?0:v==="last"?h-1:(p+v+h)%h;e.feature=e.features[g].key,s(()=>{let m=i.document.getElementById("feature-tab-"+e.feature);m&&m.focus()})},e.demoText=`# DeepLearnUtils
|
||||
`+JSON.stringify(t):""}`},[it.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${em(t)}" via a navigation guard.`},[it.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[it.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[it.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function Xn(e,t){return Ge(new Error,{type:e,[zd]:!0},t)}function ln(e,t){return e instanceof Error&&zd in e&&(t==null||!!(e.type&t))}var xh=["params","query","hash"];function em(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of xh)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function Hd(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let i=0;i<o.length;++i){let s=o[i].replace(Td," "),r=s.indexOf("="),a=No(r<0?s:s.slice(0,r)),u=r<0?null:No(s.slice(r+1));if(a in t){let p=t[a];Lt(p)||(p=t[a]=[p]),p.push(u)}else t[a]=u}return t}function zr(e){let t="";for(let o in e){let i=e[o];if(o=jh(o),i==null){i!==void 0&&(t+=(t.length?"&":"")+o);continue}(Lt(i)?i.map(s=>s&&Mr(s)):[i&&Mr(i)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+o,s!=null&&(t+="="+s))})}return t}function jd(e){let t={};for(let o in e){let i=e[o];i!==void 0&&(t[o]=Lt(i)?i.map(s=>s==null?null:""+s):i==null?i:""+i)}return t}var Gd=Symbol(""),Hr=Symbol(""),wi=Symbol(""),ki=Symbol(""),Ci=Symbol("");function Do(){let e=[];function t(i){return e.push(i),()=>{let s=e.indexOf(i);s>-1&&e.splice(s,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function yn(e,t,o,i,s,r=a=>a()){let a=i&&(i.enterCallbacks[s]=i.enterCallbacks[s]||[]);return()=>new Promise((u,p)=>{let y=h=>{h===!1?p(Xn(it.NAVIGATION_ABORTED,{from:o,to:t})):h instanceof Error?p(h):Zh(h)?p(Xn(it.NAVIGATION_GUARD_REDIRECT,{from:t,to:h})):(a&&i.enterCallbacks[s]===a&&typeof h=="function"&&a.push(h),u())},m=r(()=>e.call(i&&i.instances[s],t,o,y)),b=Promise.resolve(m);e.length<3&&(b=b.then(y)),b.catch(h=>p(h))})}function Ei(e,t,o,i,s=r=>r()){let r=[];for(let a of e)for(let u in a.components){let p=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Dd(p)){let y=(p.__vccOpts||p)[t];y&&r.push(yn(y,o,i,a,u,s))}else{let y=p();r.push(()=>y.then(m=>{if(!m)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let b=Mh(m)?m.default:m;a.mods[u]=m,a.components[u]=b;let h=(b.__vccOpts||b)[t];return h&&yn(h,o,i,a,u,s)()}))}}return r}function Wd(e,t){let o=[],i=[],s=[],r=Math.max(t.matched.length,e.matched.length);for(let a=0;a<r;a++){let u=t.matched[a];u&&(e.matched.find(y=>Yn(y,u))?i.push(u):o.push(u));let p=e.matched[a];p&&(t.matched.find(y=>Yn(y,p))||s.push(p))}return[o,i,s]}var tm=()=>location.protocol+"//"+location.host;function ou(e,t){let{pathname:o,search:i,hash:s}=t,r=e.indexOf("#");if(r>-1){let a=s.includes(e.slice(r))?e.slice(r).length:1,u=s.slice(a);return u[0]!=="/"&&(u="/"+u),Lr(u,"")}return Lr(o,e)+i+s}function nm(e,t,o,i){let s=[],r=[],a=null,u=({state:h})=>{let w=ou(e,location),A=o.value,N=t.value,R=0;if(h){if(o.value=w,t.value=h,a&&a===A){a=null;return}R=N?h.position-N.position:0}else i(w);s.forEach(v=>{v(o.value,A,{delta:R,type:yi.pop,direction:R?R>0?bi.forward:bi.back:bi.unknown})})};function p(){a=o.value}function y(h){s.push(h);let w=()=>{let A=s.indexOf(h);A>-1&&s.splice(A,1)};return r.push(w),w}function m(){if(document.visibilityState==="hidden"){let{history:h}=window;if(!h.state)return;h.replaceState(Ge({},h.state,{scroll:ys()}),"")}}function b(){for(let h of r)h();r=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",m),document.removeEventListener("visibilitychange",m)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",m),document.addEventListener("visibilitychange",m),{pauseListeners:p,listen:y,destroy:b}}function Kd(e,t,o,i=!1,s=!1){return{back:e,current:t,forward:o,replaced:i,position:window.history.length,scroll:s?ys():null}}function om(e){let{history:t,location:o}=window,i={value:ou(e,o)},s={value:t.state};s.value||r(i.value,{back:null,current:i.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function r(p,y,m){let b=e.indexOf("#"),h=b>-1?(o.host&&document.querySelector("base")?e:e.slice(b))+p:tm()+e+p;try{t[m?"replaceState":"pushState"](y,"",h),s.value=y}catch(w){console.error(w),o[m?"replace":"assign"](h)}}function a(p,y){r(p,Ge({},t.state,Kd(s.value.back,p,s.value.forward,!0),y,{position:s.value.position}),!0),i.value=p}function u(p,y){let m=Ge({},s.value,t.state,{forward:p,scroll:ys()});r(m.current,m,!0),r(p,Ge({},Kd(i.value,p,null),{position:m.position+1},y),!1),i.value=p}return{location:i,state:s,push:u,replace:a}}function su(e){e=$d(e);let t=om(e),o=nm(e,t.state,t.location,t.replace);function i(r,a=!0){a||o.pauseListeners(),history.go(r)}let s=Ge({locatLine truncated
|
||||
`);for(let b=0;b<m.length;b++){let h=m[b];if(h.startsWith("diff --git")){y(),p();continue}if(h.startsWith("--- ")){r&&r.lines.length&&y(),p().oldPath=h.replace(/^--- (a\/)?/,"").trim();continue}if(h.startsWith("+++ ")){p().newPath=h.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(h.startsWith("index ")||h.startsWith("similarity index")||h.startsWith("rename ")||h.startsWith("new file mode")||h.startsWith("deleted file mode")||h.startsWith("Binary files"))){if(h.startsWith("@@")){let w=h.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);w&&(a=parseInt(w[1],10),u=parseInt(w[2],10)),p().lines.push({kind:"hunk",oldNo:"",newNo:"",text:h});continue}r&&(h.startsWith("+")?(r.lines.push({kind:"add",oldNo:"",newNo:u,text:h.slice(1)}),u++):h.startsWith("-")?(r.lines.push({kind:"remove",oldNo:a,newNo:"",text:h.slice(1)}),a++):(r.lines.push({kind:"ctx",oldNo:a,newNo:u,text:h.startsWith(" ")?h.slice(1):h}),a++,u++))}}return o(s,r),s.join("")}})({trustAsHtml:e=>e});function Tm(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function Pm(e,t="EUR"){let o=Number(e)||0;return new Intl.NumberFormat(void 0,{style:"currency",currency:t,minimumFractionDigits:o%1?2:0,maximumFractionDigits:2}).format(o)}function Om(e){return String(e??"").toUpperCase()}function Im(e,t,o=0){return(e||[]).slice(o,o+t)}function _m(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function Mm(e,t){if(!t)return e||[];let o=Array.isArray(t)?t:[t];return[...e||[]].sort((i,s)=>{for(let r of o){let a=r[0]==="-"?-1:1;r=r.replace(/^[+-]/,"");let u=r.split(".").reduce((y,m)=>y?.[m],i),p=r.split(".").reduce((y,m)=>y?.[m],s);if(typeof u=="string"&&(u=u.toLowerCase()),typeof p=="string"&&(p=p.toLowerCase()),u!==p)return(u==null?-1:p==null?1:u<p?-1:1)*a}return 0})}function Vm(e,t){if(!e)return"";let o=new Date(e);if(isNaN(o))return String(e);if(!t||t==="mediumDate")return o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let i={yyyy:o.getFullYear(),MMM:o.toLocaleDateString(void 0,{month:"short"}),MM:String(o.getMonth()+1).padStart(2,"0"),dd:String(o.getDate()).padStart(2,"0"),d:o.getDate(),HH:String(o.getHours()).padStart(2,"0"),mm:String(o.getMinutes()).padStart(2,"0"),ss:String(o.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,s=>i[s])}function qm(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}function Yr(e){if(!e)return"";let t=/^(\d{4})-(\d{2})-(\d{2})$/.exec(String(e)),o=t?new Date(+t[1],+t[2]-1,+t[3]):new Date(e);if(isNaN(o))return"";let i=o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"}),s=new Date;s.setHours(0,0,0,0);let r=Math.round((o-s)/864e5),a;if(r===0)a="today";else if(r===1)a="tomorrow";else if(r===-1)a="yesterday";else{let u=Math.abs(r),p=Math.round(u/30.44),y=u<14?[u,"day"]:u<60?[Math.round(u/7),"week"]:p<18?[p,"month"]:[Math.round(u/365.25),"year"],m=y[0]+" "+y[1]+(y[0]===1?"":"s");a=r>0?"in "+m:m+" ago"}return i+" \xB7 "+a}function Zn(e){if(e==null||e===""||isNaN(e))return"";e=Number(e);let t="\u202F";if(e<1e3)return+(e<10?e.toFixed(2):e<100?e.toFixed(1):Math.round(e))+t+"ms";let o=e/1e3;if(o<60)return+(o<10?o.toFixed(2):o.toFixed(1))+t+"s";let i=r=>String(r).padStart(2,"0"),s=Math.round(o);return s<3600?Math.floor(s/60)+"m "+i(s%60)+"s":Math.floor(s/3600)+"h "+i(Math.floor(s%3600/60))+"m"}function Xr(e){if(!e)return"";let t=new Date(e);if(isNaN(t))return"";let o={day:"numeric",month:"short"};return t.getFullYear()!==new Date().getFullYear()&&(o.year="numeric"),t.toLocaleDateString(void 0,o)}var Ti=e=>["preparing","download","downloading","downloaded","queue","anonymizing"].includes(e)?"preparing":e==="expiring"?"expired":e==="removing"?"removed":e;function Pi(e,{flags:t,keys:o,count:i,onChange:s=()=>{}}){if(e.segView=()=>{let r=t()||{};if(o.every(u=>!!r[u]))return"all";let a=o.filter(u=>!!r[u]);return a.length===1?a[0]:"custom"},e.setSegView=r=>{let a=t();if(a){for(let u of o)a[u]=r==="all"?!0:u===r;s()}},e.segCount=r=>r==="all"?o.reduce((a,u)=>a+i(u),0):i(r),t()&&e.segView()==="custom")for(let r of o)t()[r]=!0}var uu=function(e,t,o){let i=Tt(),s=dn();vu(i,s),e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.repositories=[],e.total=-1,e.totalPage=0,e.statusCounts=[],e.totalSize=0,e.selected={},e.allSelected=!1;let r=N=>{if(N.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){N.preventDefault();let R=document.querySelector('.admin-filter-toolbar input[type="search"]');R&&R.focus()}};s(document,"keydown",r),e.on("dispose",()=>document.removeEventListener("keydown",r)),e.clearFilter=N=>{N==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[N]="",e.query.page=1},e.chips=[];let a=()=>{let N=[];e.query.owner&&N.push({key:"owner",label:"Owner",value:e.query.owner}),e.query.conference&&N.push({key:"conference",label:"Conference",value:e.query.conference}),e.chips=N};e.showSta
Line truncated
|
||||
`+N.join(`
|
||||
`)],{type:"text/csv"}),v=document.createElement("a");v.href=URL.createObjectURL(R),v.download=i.username+"-repositories.csv",v.click()},e.showStatusMessage=h=>{let w=h.statusMessage||"(no message)";window.prompt(`Status message for ${h.repoId} (${h.status}):`,w)},e.fetchGithubInfo=h=>{let w=window.open("","_blank");w&&w.document.write("<pre>Loading GitHub info for "+h.repoId+"...</pre>"),t.get("/api/admin/repos/"+h.repoId+"/github").then(A=>{w&&(w.document.open(),w.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(A.data,null,2).replace(/[<>]/g,N=>N==="<"?"<":">")+"</pre>"),w.document.close())},A=>{let N=A&&A.data?JSON.stringify(A.data,null,2):String(A);w&&(w.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+N+"</pre>")})};function p(h){t.get("/api/admin/users/"+h+"/repos",{}).then(w=>{e.repositories=w.data},w=>{console.error(w)})}function y(h){t.get("/api/admin/users/"+h,{}).then(w=>{e.userInfo=w.data},w=>{console.error(w)})}y(i.username),p(i.username),e.banUser=()=>{confirm(`Ban user ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/ban`).then(()=>y(i.username),h=>console.error(h))},e.activateUser=()=>{t.post(`/api/admin/users/${i.username}/activate`).then(()=>y(i.username),h=>console.error(h))},e.promoteUser=()=>{confirm(`Promote ${i.username} to admin?`)&&t.post(`/api/admin/users/${i.username}/promote`).then(()=>y(i.username),h=>console.error(h))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/demote`).then(()=>y(i.username),h=>console.error(h))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function m(){t.get("/api/admin/tokens").then(h=>{e.tokens=h.data||[]},h=>{h.status!==401&&h.status!==403&&console.error(h)})}m(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(h=>{e.tokenForm.plaintext=h.data.token,e.tokenForm.name="",m()},h=>console.error(h))},e.revokeToken=h=>{confirm(`Revoke token "${h.name}"?`)&&t.delete("/api/admin/tokens/"+h.id).then(()=>m(),w=>console.error(w))},e.removeCache=h=>{confirm("Remove cached files for "+h.repoId+"?")&&t.delete("/api/admin/repos/"+h.repoId).then(()=>p(i.username),w=>console.error(w))},e.removeRepository=h=>{confirm("Remove repository "+h.repoId+"?")&&t.delete("/api/repo/"+h.repoId+"/").then(()=>p(i.username),w=>console.error(w))},e.updateRepository=h=>{let w=nt({title:`Refreshing ${h.repoId}...`,date:new Date,body:`The repository ${h.repoId} is going to be refreshed.`});e.toasts.push(w),t.post(`/api/repo/${h.repoId}/refresh`).then(A=>{A.data.status=="ready"?w.title=`${h.repoId} is refreshed.`:w.title=`Refreshing of ${h.repoId}.`},A=>{w.title=`Error during the refresh of ${h.repoId}.`,w.body=A.body})},e.getGitHubRepositories=h=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(w=>{e.userInfo.repositories=w.data})};let b=null;e.watch("query",()=>{s.timeout.cancel(b),b=s.timeout(()=>{p(i.username)},500)},!0)},fu=function(e,t,o){let i=Tt(),s=dn();e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let r=A=>{if(A.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){A.preventDefault();let N=document.querySelector('.admin-filter-toolbar input[type="search"]');N&&N.focus()}};s(document,"keydown",r),e.on("dispose",()=>document.removeEventListener("keydown",r)),e.clearFilter=A=>{A==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[A]="",e.query.page=1},e.chips=[];let a=()=>{let A=[];(e.query.dateFrom||e.query.dateTo)&&A.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=A};e.statusCountFor=A=>{let N=(e.statusCounts||[]).find(R=>R._id===A);return N?N.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=A=>{e.query.sort===A?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=A,e.query.direction="desc"),e.query.page=1},e.sortIcon=A=>e.query.sort===A?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",p={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!0,expired:!0,removed:!0,error:!0,preparing:!0},y=loadFilterPrefs(u)||{};e.query=Object.assign({},p,y,{page:1,search:""});let m=o.search();m.search&&(e.query.search=m.search);let b="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(b)||"[]"),e.savePreset=()=>{let A=window.prompt("Preset name:");if(!A)return;let N=Object.assign({},e.query);delete N.page,e.presets=(e.presets||[]).filter(R=>R.name!==A),e.presets.push({name:A,query:N}),localStorage.setItem(b,JSON.stringify(e.presets))},e.applyPreset=A=>{Object.assign(e.query,A.query,{page:1})},e.deletePreset=A=>{e.presets=(e.presets||[]).filter(N=>N.name!==A.name),localStorage.setItem(b,JS
Line truncated
|
||||
`).map((le,fe)=>fe===0?le:be+le).join(`
|
||||
`)}else typeof ne=="number"||typeof ne=="boolean"?Ae=String(ne):Ae=JSON.stringify(ne);se.push(`${oe}${Ae}${x}`)}),se.push("}"),se.join(`
|
||||
`)}function y(v){let C=[],I="",V=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,q=0,W;for(;W=V.exec(v);)I+=v.slice(q,W.index),q=V.lastIndex,C.push({key:W[1],op:W[2]||"=",val:W[3]});return I+=v.slice(q),{filters:C,free:I.trim().toLowerCase()}}function m(v,C){for(let I of C.filters){let V=(W,se,Q)=>{let ne=parseFloat(W),Ee=parseFloat(se);return Q==="="?String(W)===String(se):Q==="!="?String(W)!==String(se):Q===">="?ne>=Ee:Q==="<="?ne<=Ee:Q===">"?ne>Ee:Q==="<"?ne<Ee:!0},q;if(I.key==="code")q=v.displayMessage;else if(I.key==="module")q=v.module;else if(I.key==="status")q=v._status;else if(I.key==="url")q=v._url;else if(I.key==="repo")q=v._repoId;else if(I.key==="level")q=v.level;else continue;if(q==null||!V(q,I.val,I.op))return!1}return!(C.free&&((v.displayMessage||"")+" "+(v.module||"")+" "+(v._url||"")+" "+JSON.stringify(v.raw||[])).toLowerCase().indexOf(C.free)===-1)}function b(){let v=y(e.query.search||"");e.parsedFilterCount=v.filters.length;let C=e.query.bucket,I=e.entries.filter(Q=>C&&Q._bucket!==C?!1:m(Q,v)),V=e.query.group;if(V){let Q=D=>V==="module"?D.module:D.displayMessage||D.message||"_",ne=new Map;for(let D of I){let oe=Q(D);if(ne.has(oe)){let x=ne.get(oe);x.count++,x._related.push(D),new Date(D.ts)>new Date(x.ts)&&(x.ts=D.ts,x._url=D._url,x._status=D._status),new Date(D.ts)<new Date(x._firstSeen)&&(x._firstSeen=D.ts)}else{let x=Object.assign({},D);x._key=`${V}:${oe}`,x._related=[D],x._firstSeen=D.ts,x._lastHourCount=0,x.count=1,ne.set(oe,x)}}let Ee=Date.now()-3600*1e3;for(let D of ne.values())D._lastHourCount=D._related.filter(oe=>new Date(oe.ts).getTime()>=Ee).length;I=Array.from(ne.values())}else I=I.map((Q,ne)=>(Q._key="row:"+ne+":"+Q.ts,Q._related=[Q],Q._firstSeen=Q.ts,Q._lastHourCount=0,Q.count=1,Q));let q=Q=>({error:0,warn:1,info:2})[Q._bucket]??1;e.query.sort==="count"?I.sort((Q,ne)=>q(Q)-q(ne)||ne.count-Q.count||new Date(ne.ts)-new Date(Q.ts)):I.sort((Q,ne)=>q(Q)-q(ne)||new Date(ne.ts)-new Date(Q.ts)),e.visible=I;let W=!!(e.query.bucket||(e.query.search||"").trim()),se=!W&&(e.stats?.severity?.error||0)>0&&!I.some(Q=>Q._bucket==="error")&&e.entries.length<1500;(W&&!I.length||se)&&e.canLoadMore?.()&&!e._autoLoading&&e.entries.length<2500&&(e._autoLoading=!0,h(!0))}function h(v){let C=v?e.entries.length:0,I=v?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:C,limit:I}}).then(V=>{let q=(V.data.entries||[]).map(u);e.entries=v?e.entries.concat(q):q,e.available=!!V.data.available,e.cap=V.data.max||e.cap,e.total=V.data.total||e.entries.length,e._autoLoading=!1,b()},V=>{e._autoLoading=!1,console.error(V)})}e.loadMore=()=>h(!0),e.canLoadMore=()=>e.entries.length<e.total;function w(){t.get("/api/admin/errors/stats").then(v=>{let C=v.data||{},I=C.prev24h?Math.round((C.last24h-C.prev24h)/C.prev24h*100):0;e.stats={last24h:C.last24h||0,prev24h:C.prev24h||0,delta:I,severity:C.severity||{error:0,warn:0,info:0},unique:C.unique||{error:0,warn:0,info:0},buckets:C.buckets||[],dropped:C.dropped||0},b()},v=>console.error(v))}function A(){h(),w()}e.barPx=(v,C)=>{let I=e.stats.buckets||[],V=0,q=0;for(let ne of I)V=Math.max(V,(ne.error||0)+(ne.warn||0)),q=Math.max(q,ne.info||0);let W=v[C]||0;if(!W)return 0;if(C==="info")return q?Math.max(2,Math.round(W/q*(V?16:60))):0;let se=(v.error||0)+(v.warn||0),Q=Math.round(se/V*44);return Math.max(4,Math.round(W/se*Q))},e.bucketTitle=v=>`${new Date(v.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${v.error||0} err \xB7 ${v.warn||0} warn \xB7 ${v.info||0} info`,e.toggle=v=>{e.expanded[v._key]=!e.expanded[v._key]},e.setBucket=v=>{e.query.bucket=v},e.refreshNow=A,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(A,v=>console.error(v))},e.exportCsv=()=>{let v=["ts","level","module","displayMessage","_status","_url","_repoId"],C=[v.join(",")];for(let W of e.visible)C.push(v.map(se=>{let Q=W[se]==null?"":String(W[se]);return/[",\n]/.test(Q)?`"${Q.replace(/"/g,'""')}"`:Q}).join(","));let I=new Blob([C.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),V=URL.createObjectURL(I),q=document.createElement("a");q.href=V,q.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(q),q.click(),document.body.removeChild(q),URL.revokeObjectURL(V)};function N(v){e.copyHint=`${v} copied`,s.timeout(()=>{e.copyHint=""},1500)}e.copyJson=v=>{navigator.clipboard.writeText(v._detailJson).then(()=>N("JSON"))},e.copyCurl=v=>{if(!v._url)return;let I=`curl -X ${v._method||"GET"} '${window.location.origin}${v._url}'`;navigator.clipboard.writeText(I).then(()=>N("curl"))},A();let R=i(()=>{e.query.autoRefresh&&A()},15e3);e.on("dispose",()=>i.cancel(R)),e.watch("query.search",b),e.watch("query.bucket",b),e.watch("query.sort",b),e.watch("query.group",b)},gu=function(e,t,o,i){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceTab="routes",e.cacheReport=null,e.cacheError=null,e.cacheMinutes=60,e.cacheTraffic="all",e.ovView=["performance","cache"].includes((window.location.hash||"").slice(1))?window.location.hash.slice(1):"summary",e.setOvView=function(v){e.ovView=v;try{window.history.replaceState(null,"",v==="summary"?window.location.pathname:"#"+v)}catch{}};var s=!1;e.loadCache=function(){if(!s){s=!0;var v=e.cacheMinutes;t.get("/api/admin/cache",{params:{minutes:v}}).then(function(C){v===e.cacheMinutes&&(e.cacheReport=C.data,e.cacheError=C.data.available?null:C.data.reason||"Cache monitoring is unavailable.")},function(){e.cacheError="Could not refresh cache monitoring. Displayed values may be stale."}).finally(function(){s=!1,v!==e.cacheMinutes&&e.loadCache()})}},e.cacheRows=function(){return(e.cacheReport?.rows||[]).filter(function(v){return e.cacheTraffic==="all"||v.traffic===e.cacheTraffic})},e.cacheRate=function(v,C){var I=e.cacheRows(),V=function(se){return I.filter(function(Q){return Q.name===se}).reduce(function(Q,ne){return Q+ne[C]},0)},q=V(v+"_hit"),W=V(v+"_miss");return q+W?(100*q/(q+W)).toFixed(1)+"%":"No samples"},e.cacheEventName=function(v){return v.replaceAll("_"," ")},e.cacheP95=function(v){var C=0,I=e.cacheReport?.latencyBucketsMs||[];if(!v.count)return"No samples";if(!v.ms)return"Not timed";for(var V=0;V<=I.length;V++)if(C+=v["b"+V]||0,C>=Math.ceil(v.count*.95))return V<I.length?"\u2264 "+Zn(I[V]):"> 10\u202Fs";return"No samples"},e.healthAlert=function(){let v=e.data&&e.data.system||{},C=[["Memory",v.memPercent],["Disk",v.diskPercent]].filter(([,V])=>V>90);return C.length?C.map(([V,q])=>V+" is at "+q+"%").join(" and ")+". New anonymizations may fail until "+(C.length>1?"they drop.":"it drops."):""},e.performanceError=null,e.formatMs=function(v){return v==null?"":Zn(v)},e.latencyBound=function(v,C){return v.count?v[C]==null?"> 60\u202Fs":"\u2264 "+e.formatMs(v[C]):"No samples"},e.firstByteBound=function(v){return e.latencyBound({count:v.firstByteCount,p95UpperMs:v.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(v){return Math.max(0,Math.round((Date.now()-v.sampledAt)/1e3))},e.peakRss=function(v){return(e.performance.runtimeSeries||[]).filter(function(C){return C.instance===v.instance}).reduce(function(C,I){return Math.max(C,I.rss)},v.memory.rss)},e.performanceWarning=function(v){return e.sampleAge(v)>45||v.droppedBatches||v.droppedMetrics||v.sockets?.closeWait>0||v.eventLoop?.p95Ms>100||v.memoryLimitBytes&&v.memory.rss>v.memoryLimitBytes*.8};var r=!1;e.loadPerformance=function(){var v=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==v&&(e.performance=null),!r&&(r=!0,t.get("/api/admin/performance",{params:{minutes:v}}).then(function(C){v===e.performanceMinutes&&(e.performance=C.data,e.performanceError=C.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){v===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){r=!1,v!==e.performanceMinutes&&e.loadPerformance()}))};function a(v){if(v==null)return"\u2014";for(var C=["B","KB","MB","GB","TB"],I=0,V=v;V>=1024&&I<C.length-1;)V/=1024,I++;return V.toFixed(I>0?1:0).replace(/\.0$/,"")+" "+C[I]}e.humanBytes=a;function u(v){if(!v)return"\u2014";var C=Math.floor(v/86400),I=Math.floor(v%86400/3600),V=Math.floor(v%3600/60);return C>0?C+"d "+(I<10?"0":"")+I+"h":I>0?I+"h "+(V<10?"0":"")+V+"m":V+"m"}e.humanDuration=u;function p(v){return v==null?"\u2014":v>=1e6?(v/1e6).toFixed(1)+"M":v>=1e3?(v/1e3).toFixed(1)+"K":String(v)}e.humanNum=p,e.queueTotal=function(v){return v?(v.waiting||0)+(v.active||0)+(v.delayed||0)+(v.failed||0):0},e.statusCount=function(v){if(!e.data||!e.data.repos)return 0;for(var C=e.data.repos.statusBreakdown||[],I=0;I<C.length;I++)if(C[I]._id===v)return C[I].count;return 0},e.barPct=function(v){var C=e.data&&e.data.repos?e.data.repos.total:0;if(!C)return 0;var I=[v];v==="expired"&&I.push("expiring"),v==="removed"&&I.push("re
Line truncated
|
||||
`);t.innerHTML=o.map(i=>{let s=i.replace(/^(<[^>]+>)+/,"").charAt(0),r=i.startsWith("@@")?"hunk":s==="+"?"add":s==="-"?"remove":"";return`<span class="md-diff-line${r?" is-"+r:""}">${i||" "}</span>`}).join(`
|
||||
`)}),ku=(e,t=0)=>{let o=(String(e).slice(t).match(/^[ \t]*/)[0]||"").replace(/\t/g," ").length;return Math.min(t+o+4,24)},bs=e=>{e?.closest?.(".reviewer-preview")&&e.querySelectorAll("pre code").forEach(t=>{let o=t.querySelectorAll(".md-diff-line");if(o.length){o.forEach(s=>{s.classList.contains("is-hunk")||s.style.setProperty("--hang",ku(s.textContent,1)+"ch")});return}if(t.querySelector(".code-line"))return;let i=[];t.innerHTML=t.innerHTML.replace(/\n$/,"").split(`
|
||||
`).map(s=>{let r=i.join("");s.replace(/<\/?span\b[^>]*>/g,u=>(u.startsWith("</")?i.pop():i.push(u),u));let a=s.replace(/<[^>]+>/g,"");return`<span class="code-line" style="--hang:${ku(a)}ch">${r}${s||" "}${"</span>".repeat(i.length)}</span>`}).join(`
|
||||
`)})},Su={props:["content","terms","options"],setup(e){let t=vt(null),o=()=>{t.value.innerHTML=renderMD(e.content||"",window.location.pathname+"/../"),Eu(t.value),bs(t.value)};return on(()=>{o(),st(()=>[e.content,e.terms,e.options],o,{deep:!0})}),()=>Oe("markdown",{ref:t})}},xm={props:["file","terms","options"],setup(e){let t=vt(null),o=()=>ot(()=>{t.value?.querySelectorAll("pre code").forEach(i=>window.Prism?.highlightElement(i)),Eu(t.value),bs(t.value)});return on(o),st(()=>[e.file?.content,e.terms,e.options],o,{deep:!0}),()=>{let i=e.file||{},s=(i.filename||"").split(".").pop().toLowerCase();if(["md","markdown"].includes(s)||i.language==="Markdown")return Oe("gist-file",{ref:t},Oe(Su,{content:i.content,terms:e.terms,options:e.options}));let r={js:"javascript",jsx:"javascript",ts:"javascript",typescript:"javascript",py:"python",html:"markup",xml:"markup",svg:"markup",sh:"bash"},a=(i.language||s||"none").toLowerCase();return Oe("gist-file",{ref:t},Oe("pre",{class:"line-numbers"},Oe("code",{class:"language-"+(r[a]||a),key:i.content},i.content||"")))}}},eg=e=>e?.querySelectorAll(".nb-input pre").forEach(t=>{if(t.parentElement?.classList.contains("nb-code-scroll"))return;let o=document.createElement("div");o.className="nb-code-scroll",t.replaceWith(o),o.appendChild(t);let i=()=>{let s=t.scrollWidth-t.clientWidth>1;if(o.classList.toggle("is-overflowing",s),o.classList.toggle("is-at-end",s&&t.scrollLeft+t.clientWidth>=t.scrollWidth-2),!s)return;let r=t.clientWidth-16,a=Math.max(24,r*t.clientWidth/t.scrollWidth);o.style.setProperty("--thumb-w",a+"px"),o.style.setProperty("--thumb-x",(r-a)*t.scrollLeft/(t.scrollWidth-t.clientWidth)+"px")};t.addEventListener("scroll",i,{passive:!0}),window.ResizeObserver?new ResizeObserver(i).observe(t):i(),i()}),tg={props:["file","content"],setup(e){let t=vt(null),o=0,i,s=async()=>{let r=++o;i?.abort(),i=new AbortController;try{let a=e.content?JSON.parse(e.content):await fetch(e.file?.download_url||e.file,{signal:i.signal}).then(u=>{if(!u.ok)throw Error("Notebook request failed");return u.json()});if(await Ln("notebook"),r!==o)return;t.value.innerHTML=Oi(DOMPurify.sanitize(nb.parse(a).render())),t.value.querySelectorAll("pre code").forEach(u=>window.Prism?.highlightElement(u)),eg(t.value)}catch(a){r===o&&a.name!=="AbortError"&&(t.value.textContent="Unable to render the notebook.")}};return on(()=>{s(),st(()=>[e.file,e.content],s)}),Kn(()=>{o++,i?.abort()}),()=>Oe("notebook",{ref:t})}},ng={props:["stats"],setup(e){return()=>{let t=Object.entries(e.stats||{}).filter(([,i])=>i.code),o=t.reduce((i,[,s])=>i+s.code,0);return Oe("loc",t.map(([i,s])=>Oe("div",{class:"lang",title:`${i}: ${s.code.toLocaleString()} lines`,style:{width:s.code*100/o+"%",background:langColors[i]}})))}}},Nu={Markdown:Su,GistFile:xm,Notebook:tg,Loc:ng,HtmlDoc:yu,QuotaUsage:Qm,Pdfviewer:_l(async()=>(await Ln("pdf"),pdfjsLib.GlobalWorkerOptions.workerSrc="/script/external/pdf.worker.js",bu))},Ru={async mounted(e,{value:t}){e._editorValue=t;try{if(await qs(),e._editorDisposed)return;let o=e._editorValue,i=ace.edit(e);e._editor=i,i.setValue(String(o.content??""),-1),Cu(e,o.options),e._editorCleanup=o.options?.onLoad?.(i)}catch(o){e._editorDisposed||(e.textContent=o.message)}},updated(e,{value:t}){e._editorValue=t,e._editor&&(e._editor.getValue()!==String(t.content??"")&&e._editor.setValue(String(t.content??""),-1),Cu(e,t.options))},beforeUnmount(e){e._editorDisposed=!0,e._editorCleanup?.(),e._editor?.destroy()}};function Cu(e,t={}){let o=Zm.has(t.mode)?t.mode:"text";e._editor.session.setMode("ace/mode/"+o),t.theme&&e._editor.setTheme("ace/theme/"+t.theme),e._editor.setReadOnly(t.readOnly!==!1)}var Du={mounted(e){if(!window.IntersectionObserver)return;let t=[...e.querySelectorAll('a[href^="#"]')],o=new Set,i=new IntersectionObserver(s=>{s.forEach(a=>a.isIntersecting?o.add(a.target.id):o.delete(a.target.id));let r=t.find(a=>o.has(a.hash.slice(1)));r&&t.forEach(a=>a.classList.toggle("active",a===r))},{rootMargin:"-20% 0px -60% 0px",threshold:0});ot(()=>{t.forEach(s=>{let r=document.getElementById(s.hash.slice(1));r&&i.observe(r)}),t[0]&&!t.some(s=>s.classList.contains("active"))&&t[0].classList.add("active")}),e._observer=i},beforeUnmount(e){e._observer?.disconnect()}};var Qr=(e,t,o)=>{let i=e instanceof RegExp?Au(e,o):e,s=t instanceof RegExp?Au(t,o):t,r=i!==null&&s!=null&&og(i,s,o);return r&&{start:r[0],end:r[1],pre:o.slice(0,r[0]),body:o.slice(r[0]+i.length,r[1]),post:o.slice(r[1]+s.length)}},Au=(e,t)=>{let o=t.match(e);return o?o[0]:null},og=(e,t,o)=>{let i,s,r,a,u,p=o.indexOf(e),y=o.indexOf(t,p+1),m=p;if(p>=0&&y>0){if(e===t)return[p,y];for(i=[],r=o.length;m>=0&&!u;){if(m===p)i.push(m),p=o.indexOf(e,m+1);else if(i.length===1){let b=i.pop();b!==void 0&&(u=[b,y])}else s=i.pop(),s!==void 0&&s<r&&(r=s,a=y),y=o.indexOf(t,m+1);m=p<y&&p>=0?p:y}i.length&&a!==void 0&&(u=[r,a])}return u};var Tu="\0SLASH"+Math.random()+"\0",Pu="\0OPEN"+Math.random()+"\0",ta="\0CLOSE"+Math.random()+"\0",Ou="\0COMMA"+Math.random()+"\0",Iu="\0PE
Line truncated
|
||||
`)},sa=class{constructor(t){Je(this,"gen");Je(this,"threads",[]);Je(this,"match",!1);Je(this,"runs",[]);Je(this,"ids",[]);this.gen=t}get dead(){return!this.threads.length&&!this.match&&!this.runs.length}},zu=(e,t)=>e.ids.length!==t.ids.length||e.runs.length!==t.runs.length||e.ids.some((o,i)=>o!==t.ids[i])?!1:e.runs.every(o=>t.runs.some(i=>i.neg===o.neg&&zu(i.state,o.state))),Vu=e=>({has:new Float64Array(e),seen:new Float64Array(e),gen:0}),eo,On,ia,ra,Es=class{constructor(t,o,i){Ve(this,On);Je(this,"dots");Je(this,"dotStart");Ve(this,eo);this.dots=t,this.dotStart=o,Ue(this,eo,i)}state(){return new sa(++re(this,eo).gen)}done(t){return t.ids=t.threads.map(o=>o.id).sort((o,i)=>o-i),t.match&&t.ids.unshift(oa.id),t}restricted(t,o){return o&&(this.dots||this.dotStart&&t===0)}add(t,o,i,s){switch(o.t){case"split":if(!qe(this,On,ia).call(this,t,o))return;for(let a of o.out)this.add(t,a,i,s);return;case"neg":if(!qe(this,On,ia).call(this,t,o))return;let r=this.state();return this.add(r,o.sub,i,!1),this.done(r),this.addRun(t,{t:"run",neg:o,state:r},i,s);case"star":if(this.restricted(i,s))return;qe(this,On,ra).call(this,t,o)&&this.add(t,o.next,i,s);return;case"match":t.match=!0;return;default:qe(this,On,ra).call(this,t,o)}}addRun(t,o,i,s){for(let r of t.runs)if(r.neg===o.neg&&zu(r.state,o.state))return;t.runs.push(o),o.state.match||this.add(t,o.neg.next,i,s)}step(t,o,i,s){let r=this.state();for(let a of t.threads)switch(a.t){case"lit":da(a,o)&&this.add(r,a.next,i+1,s);break;case"any":this.restricted(i,s)||this.add(r,a.next,i+1,s);break;case"class":!this.restricted(i,s)&&a.re.test(o)&&this.add(r,a.next,i+1,s);break;case"star":this.restricted(i,s)||this.add(r,a,i+1,s)}if(!this.restricted(i,s))for(let a of t.runs){if(a.state.dead){this.addRun(r,a,i+1,s);continue}let u=this.step(a.state,o,i,!1);this.addRun(r,{t:"run",neg:a.neg,state:u},i+1,s)}return this.done(r)}};eo=new WeakMap,On=new WeakSet,ia=function(t,o){let{seen:i}=re(this,eo);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,!0)},ra=function(t,o){let{has:i}=re(this,eo);return i[o.id]===t.gen?!1:(i[o.id]=t.gen,t.threads.push(o),!0)};var Hu=e=>{let t=e.ids.join();if(!e.runs.length)return t;let o=e.runs.map(i=>`${i.neg.id}(${Hu(i.state)})`);return`${t}|${o.sort().join("|")}`},_g=1<<18,Ao,To,Rn,to,Dn,An,Tn,Kt,la,Mi,ju,aa=class{constructor(t,o){Ve(this,Kt);Ve(this,Ao);Ve(this,To,new Map);Ve(this,Rn,0);Ve(this,to);Ve(this,Dn);Ve(this,An);Ve(this,Tn);Ue(this,Ao,t),Ue(this,An,new Es(!1,!1,o)),Ue(this,Tn,new Es(!1,!0,o))}start(t){if(t){if(re(this,Dn))return re(this,Dn);let i=re(this,Tn).state();return re(this,Tn).add(i,re(this,Ao),0,!0),Ue(this,Dn,qe(this,Kt,la).call(this,re(this,Tn).done(i)))}if(re(this,to))return re(this,to);let o=re(this,An).state();return re(this,An).add(o,re(this,Ao),0,!0),Ue(this,to,qe(this,Kt,Mi).call(this,re(this,An).done(o)))}next(t,o){let i=o.charCodeAt(0),s=i<128?void 0:t.next.get(o);if(s)return s;let r=t===re(this,Dn)?qe(this,Kt,Mi).call(this,re(this,Tn).step(t.state,o,0,!0)):qe(this,Kt,Mi).call(this,re(this,An).step(t.state,o,1,!0));return i<128?(t.ascii||(t.ascii=new Array(128),Ue(this,Rn,re(this,Rn)+128)),t.ascii[i]=r):t.next.set(o,r),Pa(this,Rn)._++,r}};Ao=new WeakMap,To=new WeakMap,Rn=new WeakMap,to=new WeakMap,Dn=new WeakMap,An=new WeakMap,Tn=new WeakMap,Kt=new WeakSet,la=function(t){return{state:t,match:t.match,dead:t.dead,next:new Map}},Mi=function(t){let o=Hu(t),i=re(this,To).get(o);if(i)return i;re(this,Rn)>_g&&qe(this,Kt,ju).call(this);let s=qe(this,Kt,la).call(this,t);return re(this,To).set(o,s),Ue(this,Rn,re(this,Rn)+(o.length+1)),s},ju=function(){re(this,To).clear(),Ue(this,Rn,0),Ue(this,to,void 0),Ue(this,Dn,void 0)};var Mg=e=>{let t=[];for(let o=e;o.t!=="match";o=o.next){if(o.t==="split"||o.t==="neg")return;t.push(o)}return t},Vg=(e,t,o,i)=>{let s=0,r=o,a=-1,u=0;for(;r<i;){let p=e[s];if(p?.t==="star"){a=s++,u=r;continue}let y=t[r];if(p&&(p.t==="any"||p.t==="lit"&&da(p,y)||p.t==="class"&&p.re.test(y))){s++,r++;continue}if(a===-1)return!1;s=a+1,r=++u}for(;e[s]?.t==="star";)s++;return s===e.length},Gu=e=>{let t=0,o=0;for(let i of e){let[s,r]=qg(i);t+=s,o+=r}return[t,o]},qg=e=>{if(e.t==="star")return[0,1/0];if(e.t!=="ext")return[1,1];if(e.type==="!")return[0,1/0];let t=e.alts.map(Gu),o=Math.min(...t.map(r=>r[0])),i=Math.max(...t.map(r=>r[1])),s=i===0?0:1/0;return e.type==="@"?[o,i]:e.type==="?"?[0,i]:e.type==="*"?[0,s]:[o,s]},qu=(e,t,o)=>{for(let i=0;i<e.length;i++)if(!da(e[i],t[o+i]))return!1;return!0},$g=/[/\ud800-\udfff]/,Lg=/[\ud800-\udfff]/,_i=e=>Lg.test(e)?[...e]:e,$u=e=>{if(e.includes("/"))throw new Error("can only match a single path portion, got: "+e)},Pn,Po,no,Vi,oo,Ns,qi,so,Rs,Ds,As,wn,un,Oo,Io,Ts,$i,Wt,io,Cs,Ss=class{constructor(t,o={}){Ve(this,io);Je(this,"pattern");Je(this,"options");Ve(this,Pn);Ve(this,Po);Ve(this,no);Ve(this,Vi);Ve(this,oo);Ve(this,Ns,!1);Ve(this,qi);Ve(this,so);Ve(this,Rs,!1);Ve(this,Ds,0);Ve(this,As,1/0);Ve(this,wn,[]);Ve(this,un,[]);Ve(this,Oo);Ve(this,Io);Ve(this,Ts,!1);Ve(this,$i);Ve(this,Wt);if(t.includes
Line truncated
|
||||
`).forEach((o,i)=>{let{term:s}=ka(o.trim());if(!s)return;let r=[];for(let a=0;a<s.length;a++){if(s[a]==="\\"){a++;continue}lc.has(s[a])&&!r.includes(s[a])&&r.push(s[a])}r.length&&t.push({line:i,term:s,symbols:r})}),t}function uc(e,t){let o=String(e||"").split(`
|
||||
`),i=o[t];if(i===void 0)return e;let{term:s}=ka(i.trim());if(!s)return e;let r="";for(let u=0;u<s.length;u++){if(s[u]==="\\"){r+=s.slice(u,u+2),u++;continue}r+=lc.has(s[u])||s[u]==="."?"\\"+s[u]:s[u]}let a=i.indexOf(s);return o[t]=i.slice(0,a)+r+i.slice(a+s.length),o.join(`
|
||||
`)}var lo={VIEWER_ERRORS:{title:"Content could not be loaded",access_title:"GitHub access needs attention",reader_help:"Reading this repository? You do not need to sign in. Try again after the owner restores access, or let them know this anonymous link is unavailable.",owner_help:"Own this repository? If your GitHub account was suspended or recently recovered, sign in to Anonymous GitHub again and check the GitHub connection and repository permissions in your dashboard. Then retry this page. Keep the existing anonymous URL; do not delete the repository to restore access.",retry:"Try again",dashboard:"Open owner dashboard"},ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"We could not load this content. Try again. If it still fails, contact support with the anonymous URL and the time of the error.",unreachable:"The service could not complete this request. Wait a moment and try again.",request_error:"The connection was interrupted. Check your internet connection and try again.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"Anonymous GitHub cannot access the source repository on GitHub. It may be private, unavailable, or require the owner to reconnect GitHub.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub temporarily blocked the request because we hit its API rate limit. Wait a few minutes and try again. If the problem persists and you contact GitHub Support, include the request ID and timestamp shown in GitHub's response (the 'X-GitHub-Request-Id' header).",rate_limited:"GitHub API rate limit reached. The repository will be available shortly \u2014 please wait a moment and refresh.",repoId_already_used:"The repository ID is already used.",invalid_repoId:"The format of the repository ID is invalid.",unsupported_source:"The repository source type is not supported.",branch_not_specified:"The branch is not specified.",branch_not_found:"The branch of the repository cannot be found.",commit_not_specified:"A commit must be specified.",commit_not_found:"The configured commit no longer exists in the source repository. It may have been force-pushed, rebased, or removed.",repo_renamed:"The source repository appears to have been renamed or moved on GitHub. Please refresh and try again \u2014 the cached name has been updated.",invalid_commit_format:"The commit hash format is invalid. It must be a hexadecimal string.",pull_request_not_found:"The requested pull request could not be found.",pull_request_expired:"This pull request has expired and is no longer available.",pull_request_not_ready:"This pull request is still being prepared. Please t
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,b.options),u.restore()})}p();let y=null;e.saveDefault=m=>{if(m&&m.preventDefault&&m.preventDefault(),e.saving)return;let b=u.checkpoint(),h={terms:e.terms.split(`
|
||||
`).map(w=>w.trim()).filter(w=>w.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",h).then(()=>{u.accept(b),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",y&&i.cancel(y),y=i(()=>{e.message=null},2500)},w=>{e.saving=!1;let A=w&&w.data&&w.data.error,N="Unable to save your defaults. Please try again.";Promise.all([o("ERRORS."+A),o("ERRORS.unknown_error")]).then(([R,v])=>{e.error=A&&R!==v?R:N},()=>{e.error=N})})},e.deleteAccount=()=>{e.deletingAccount||!e.confirmDeleteAccount||e.deleteUsername!==e.user?.username||(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},vc=function(e,t,o){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(i=>{o.url("/dashboard")},i=>{e.error=i.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},yc=function(e,t,o,i,s){e.user&&!e.user.status&&o.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&o.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,p){let y={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(y===void 0)return;u.preventDefault();let m=e.features.length,b=y==="first"?0:y==="last"?m-1:(p+y+m)%m;e.feature=e.features[b].key,s(()=>{let h=i.document.getElementById("feature-tab-"+e.feature);h&&h.focus()})},e.demoText=`# DeepLearnUtils
|
||||
Developed by Jane Smith at MIT CSAIL.
|
||||
See Smith et al., "Attention That Works," 2026.
|
||||
Contact: jane.smith@mit.edu`,e.demoTerms="Jane Smith, Smith, MIT CSAIL, jane.smith@mit.edu",e.demoParts=function(){let u=(e.demoTerms||"").split(/[,\n]/).map(m=>m.trim()).filter(Boolean).sort((m,k)=>k.length-m.length),p=m=>m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!u.length)return[{text:e.demoText}];let v=new RegExp("("+u.map(p).join("|")+")","gi"),h={},g=0;return(e.demoText||"").split(v).filter(m=>m!=="").map(m=>{let k=u.find(T=>T.toLowerCase()===m.toLowerCase());return k?(h[k.toLowerCase()]||(h[k.toLowerCase()]=++g),{text:String(h[k.toLowerCase()]),redacted:!0,original:m}):{text:m}})},e.demoCount=()=>e.demoParts().filter(u=>u.redacted).length,e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function r(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests}).catch(()=>{})}r();function a(u){let p={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return p;let v=new Array(u.length-1);for(let w=1;w<u.length;w++)v[w-1]=u[w]-u[w-1];let h=v.length,g=Math.max.apply(null,v),m=Math.min.apply(null,v),k=Math.min(0,m),T=g-k||1;p.viewW=h*2,p.bars=new Array(h);for(let w=0;w<h;w++){let b=(v[w]-k)/T,E=Math.max(1.5,b*34);p.bars[w]={x:(w*2+.25).toFixed(2),y:(36-E).toFixed(2),w:"1.5",h:E.toFixed(2)}}if(p.deltaToday=v[h-1],h>=2){let w=v[h-2];w&&(p.pctChange=(p.deltaToday-w)/w*100)}return p.pctAbs=Math.round(Math.abs(p.pctChange)),p.isUp=p.pctChange>=0,p}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let p=u.data||[];e.history={repositories:a(p.map(v=>v.nbRepositories||0)),users:a(p.map(v=>v.nbUsers||0)),pageViews:a(p.map(v=>v.nbPageViews||0)),pullRequests:a(p.map(v=>v.nbPullRequests||0))}}).catch(()=>{})},Ku=function(e,t,o,i,s,r){let a=Tt();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,nt(()=>document.querySelector(".dashboard-quota > summary")?.focus())},bn()(document,"click",D=>{e.quotaExpanded&&!D.target.closest?.(".dashboard-quota")&&(e.quotaExpanded=!1)}),e.filtersExpanded=!1,e.projectView="all",e.selectedProject=null,e.projectName="",e.nameSaving=!1,e.nameError="",e.nameMessage="",e.needsAttention=D=>D.status==="error"||D._broken||p(D.status)==="progress"&&D._stale,e.attentionCount=()=>e.dashboardTotals?.attention??e.items.filter(e.needsAttention).length,e.hasProjects=()=>(e.dashboardTotals?.total??e.items.length)>0,e.setProjectView=D=>{e.projectView=D,D==="attention"&&(e.filters.status.error=!0,e.filters.status.progress=!0)},e.projectDetailsId=D=>"project-details-"+encodeURIComponent(D._type+":"+(D._id||D._source||"unknown")),e.showProjectDetails=async D=>{e.selectedProject=D,e.projectName=D.projectName||"",e.nameError="",e.nameMessage="",await nt();let X=document.getElementById(e.projectDetailsId(D));X?.focus(),X?.scrollIntoView?.({block:"nearest"})},e.closeProjectDetails=D=>{e.selectedProject=null,nt(()=>document.querySelector(`[data-project-actions="${encodeURIComponent(D._type+":"+D._id)}"]`)?.focus())},e.saveProjectName=async D=>{if(e.nameSaving||!D._id)return;let X=e.projectName.trim();if(X.length>100){e.nameError="Use 100 characters or fewer.";return}e.nameSaving=!0,e.nameError="",e.nameMessage="";try{let le=await t.post("/api/user/project-name",{type:D._type,id:D._id,name:X});D.projectName=le.data.name,D._label=D.projectName||D._fallbackLabel,e.selectedProject===D&&(e.nameMessage=X?"Project name saved.":"Default project name restored."),N||(e.dashboardCursor=null,await q(),e.selectedProject===D&&(e.selectedProject=e.items.find(Ee=>Ee._type===D._type&&Ee._id===D._id)||null))}catch{e.selectedProject===D&&(e.nameError="The project name could not be saved. Try again.")}finally{e.nameSaving=!1}},e.copyingId=null,e.copyLink=async D=>{if(!D._viewUrl||e.copyingId)return;let X=new URL(D._viewUrl,s.location.origin).href;e.copyingId=D._type+":"+D._id;try{await Bu(X),e.addToast({title:"Link copied",body:"The anonymous link is ready to share."})}catch{e.addToast({title:"Copy this anonymous link",body:X})}finally{e.copyingId=null}},e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"In progress",error:"Error",expired:"Expired",removed:"Removed"};let u=["queue","download","downloaded","preparing","anonymizing"];function p(D){return D==="ready"||D==="error"?D:D==="expired"||D==="expiring"?"expired":D==="removed"||D==="removing"?"removed":(u.indexOf(D)>-1,"progress")}let v=7200*1e3,h="dashboard.filterPrefs",g={typeFilter:"all",filters:{status:{ready:!0,progress:!0,error:!0,e
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,F.options),e.options.expirationDate=F.options&&F.options.expirationDate?new Date(F.options.expirationDate):G(),!i.repoId&&!i.pullRequestId&&!i.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),S&&S()})}function ae(S,_,F){e.anonymize&&e.anonymize[S]&&e.anonymize[S].setValidity(_,F)}function he(S){try{let _=parseGithubUrl(S);if(_&&_.owner&&_.repo)return _.owner+"/"+_.repo}catch{}return null}e.accessRepairAllowed=!1,e.repairingGitHubAccess=!1,e.accessRepairMessage="",e.repairGitHubAccess=async()=>{if(!e.accessRepairAllowed||e.repairingGitHubAccess||!e.githubConnections?.appEnabled||!i.repoId||e.role!=="owner"||e.githubConnection!=="github-app"||he(e.sourceUrl)!==e._originalFullName)return;e.repairingGitHubAccess=!0,e.accessRepairMessage="";let S=m();try{let _=await t.get("/github/connections");if(e.githubConnections=_.data,!_.data.appEnabled)return;let F=await t.post("/github/connections/migrate",{type:"repository",id:i.repoId,connection:"github-app"},{headers:{"X-CSRF-Token":_.data.csrf}});if(!g(S))return;e.error=null,F.data.fullName&&(e._originalFullName=F.data.fullName,e.source.fullName=F.data.fullName,e.sourceUrl="https://github.com/"+F.data.fullName),e.accessRepairMessage="GitHub access repaired. Your anonymous URL and published commit are unchanged.",await e.urlSelected(!0)}catch{g(S)&&(e.accessRepairMessage="Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced.")}finally{e.repairingGitHubAccess=!1,k(S)}},e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await Re()}catch{}}};function Ce(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:he(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function Pe(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function Ne(){let S=new URLSearchParams(window.location.search).get("billingDraft");if(S){b.restore();try{let F=(await t.get("/api/billing/draft/"+encodeURIComponent(S))).data;if(F.path!==s.path())throw new Error("Wrong task");E=F.values.conference||null;for(let Q of["sourceUrl","terms","repoId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(F.values,Q)&&(e[Q]=F.values[Q]);return e.options?.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),N=F.id,O=JSON.stringify(b.checkpoint()),e.draftRestored=!0,e.hostingReview=!0,e.sourceUrl&&await Pe(),s.replace?.(s.path()),!0}catch{e.billingError="The checkout draft could not be restored. Retry by reloading this page; any local draft is still available."}}let _;try{_=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!_||_.path!==s.path()||Date.now()-_.savedAt>=30*6e4)return b.restore()?(e.sourceUrl&&await Pe(),!0):!1;b.restore();for(let F of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(_.draft,F)&&(e[F]=_.draft[F]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await Pe(),sessionStorage.removeItem("github-access-draft"),!0}Ee(()=>{!i.repoId&&!i.pullRequestId&&!i.gistId&&a(Ne,0),i.repoId&&i.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=i.repoId,t.get("/api/repo/"+e.repoId).then(async S=>{e.accessRepairAllowed=!["removed","archived","preparing","queue","download","removing","expiring"].includes(S.data.status),e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.fullName,e._originalFullName=S.data.source.fullName,e.terms=S.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=S.data.source,e.role=S.data.role||"owner",e._existingNever=S.data.options.expirationMode==="never",e._savedExpirationDate=S.data.options.expirationDate?new Date(S.data.options.expirationDate):null,e.coauthors=S.data.coauthors||[],e._originalBranch=S.data.source.branch,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,e.repositoryID=S.data.source.repositoryID,e._originalRepositoryID=S.data.source.repositoryID,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&(await Promise.all([Re(),Qe()]),j())},()=>{s.url("/404")})),i.pullRequestId&&i.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=i.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async S=>{if(e.githubConnection=S.data.connection||"oauth",e.sourceUrl="https://github.com/"+S.data.source.repositoryFullName+"/pull/"+S.data.source.pullRequestId,e.terms=S.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne())try{await ee()}catch{}},()=>{s.url("/404")})),i.gistId&&i.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=i.gistId,t.get("/api/gist/"+e.gistId).then(async S=>{e.sourceUrl="https://gist.github.com/"+S.data.source.gistId,e.terms=S.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=S.data.source,e.options=Object.assign({},e.options,S.data.options),e.conference=S.data.conference,e._originalConference=S.data.conference,S.data.options.expirationDate&&(e.options.expirationDate=new Date(S.data.options.expirationDate)),T=w(parseGithubUrl(e.sourceUrl)),!await Ne()&&await fe()},()=>{s.url("/404")}))}),t.get("/github/connections").then(S=>{e.githubConnections=S.data,!i.repoId&&!i.pullRequestId&&!i.gistId&&e.githubConnection===void 0&&S.data.appEnabled&&S.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&Pe())}).catch(()=>{}),e.urlSelected=async(S=!1)=>{let _=++p;e.previousConfigurations=[],e.previousConfigurationId="";let F;try{F=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,V.cancel(),ne.cancel(),Le.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",ae("sourceUrl","github",!1);return}let Q=w(F),pe=S||T===Q;T=Q,e.sourceUrlError="",ae("sourceUrl","github",!0);let be=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,setTimeout(()=>{let Xe=window.document.getElementById("sourceUrl");if(Xe){try{Xe.setSelectionRange(0,0)}catch{}Xe.scrollLeft=0}},50),be&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!pe&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],pe||(e.source={type:"GitHubStream",branch:"",commit:""}),V.cancel(),ne.cancel(),Le.cancel(),K.clear(),ye.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=pe;try{F.gistId&&!F.repo?(e.detectedType="gist",e.source={gistId:F.gistId},await fe()):F.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:F.owner+"/"+F.repo,pullRequestId:F.pullRequestId},await ee()):(e.detectedType="repo",le(),await Promise.all([Re(),Qe()]),j())}catch{}finally{_===p&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let S=e.branches.filter(F=>F.name==e.source.branch)[0];if(!S)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=S.commit),e.readme=S.readme,await Qe(),j()}),e.getBranches=async S=>{let _=parseGithubUrl(e.sourceUrl),F=m();try{let Q=await t.get(`/api/repo/${_.owner}/${_.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0||e.reconnectingSource?"1":"0",repositoryID:Ce()}});if(!g(F))return;e.branches=Q.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let pe=e.branches.filter(be=>be.name==e.source.branch);pe.length>0&&(!(!e.reconnectingSource&&!S&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=pe[0].commit),e.readme=pe[0].readme,await Qe(S))}catch(Q){if(!g(F))return;e.branches=[],e.sourceUnreachable=Q&&(Q.status===404||Q.data&&Q.data.error==="repo_not_found");let pe=Q&&Q.data&&Q.data.error||(Q&&Q.status===404?"repo_not_found":"unknown_error");r("ERRORS."+pe).then(be=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:be}),e.error=be},console.error),typeof ae=="function"&&ae("sourceUrl","missing",!1)}finally{k(F)}};async function Re(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye();let F=await t.get(`/api/repo/${S.owner}/${S.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,repositoryID:Ce(),force:"1"}});if(!g(_))return;e.details=F.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(F){if(!g(_))return;throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}async function Qe(S){if(e.readme&&!S)return e.readme;let _=parseGithubUrl(e.sourceUrl),F=m();try{let Q=await t.get(`/api/repo/${_.owner}/${_.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:S===!0?"1":"0",branch:e.source.branch,repositoryID:Ce()}});g(F,!0)&&(e.readme=Q.data)}catch{g(F,!0)&&(e.readme="")}finally{k(F)}}let Ke={magicalBraces:!0},rt=S=>S.startsWith("/")&&!S.endsWith("/")&&On(so(S.slice(1),Ke),Ke)===S.slice(1),Ze=()=>Object.keys(e.preserve.picked).filter(S=>e.
Line truncated
|
||||
`).map(S=>S.trim()).filter(Boolean),te=null,L=0,x=null,H=()=>!1;function ue(){let S=L+"\0"+e.preserve.patterns;if(S!==x){x=S;try{H=ua(da([...Ze(),...Z()]))}catch{H=()=>!1}}return H}function we(S){let _=new Map([["",{dirs:[],files:[]}]]);for(let F of S){let Q=F.split("/"),pe="";for(let be=0;be<Q.length-1;be++){let Xe=Q.slice(0,be+1).join("/");_.has(Xe)||(_.set(Xe,{dirs:[],files:[]}),_.get(pe).dirs.push(Xe)),pe=Xe}_.get(pe).files.push(F)}for(let F of _.values())F.dirs.sort();return{files:S,dirs:_,descendants:new Map}}function Oe(S){return te.descendants.has(S)||te.descendants.set(S,te.files.filter(_=>_.startsWith(S+"/"))),te.descendants.get(S)}let C=S=>ue()(S);function R(S){let _=Oe(S),F=0;for(let Q of _)C(Q)&&F++;return F===0?"none":F===_.length?"all":"some"}e.preserve={open:!1,loading:!1,error:"",truncated:!1,commit:"",search:"",expanded:{},picked:{},patterns:"",patternsOpen:!1},e.preserveSummary=()=>{let S=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean).map(_=>rt(_)?so(_.slice(1),Ke):_);return S.length?S.slice(0,2).join(", ")+(S.length>2?` and ${S.length-2} more`:""):"None. Every file is redacted."};async function I(){let S=e.source?.commit;if(!(te&&e.preserve.commit===S)){e.preserve.loading=!0,e.preserve.error="",te=null;try{let _=parseGithubUrl(e.sourceUrl),F=await t.get(`/api/repo/${_.owner}/${_.repo}/tree`,{params:{commit:S,connection:e.githubConnection,anonymizedRepoId:e.isUpdate&&i.repoId&&he(e.sourceUrl)===e._originalFullName?i.repoId:void 0,repositoryID:Ce()}});if(e.source?.commit!==S)return;te=go(we(F.data.files||[])),e.preserve.commit=S,e.preserve.truncated=!!F.data.truncated}catch{e.preserve.error="The file list could not be loaded from GitHub. You can still use patterns.",e.preserve.patternsOpen=!0}finally{e.preserve.loading=!1}}}e.openPreserveFiles=()=>{let S={},_=[];for(let Q of e.options?.preserveFiles||[]){let pe=String(Q).trim();pe&&(!_.length&&rt(pe)?S[so(pe.slice(1),Ke)]=!0:_.push(pe))}let F={};for(let Q of Object.keys(S)){let pe=Q.split("/");for(let be=1;be<pe.length;be++)F[pe.slice(0,be).join("/")]=!0}Object.assign(e.preserve,{open:!0,search:"",picked:S,expanded:F,patterns:_.join(`
|
||||
`),patternsOpen:_.length>0}),L++,a(()=>{let Q=document.getElementById("preserve-files-dialog");Q?.showModal&&!Q.open&&Q.showModal(),document.getElementById("preserve-files-search")?.focus()},0),I()},e.closePreserveFiles=()=>{document.getElementById("preserve-files-dialog")?.close?.(),e.preserve.open=!1},e.savePreserveFiles=()=>{e.options.preserveFiles=da([...Ze(),...Z()]),e.closePreserveFiles()},e.preserveRows=()=>{if(!te)return[];let S=ue(),_=(be,Xe,Yt)=>{let wt=!!e.preserve.picked[be],$n=S(be);return{kind:"file",path:be,label:Yt,depth:Xe,state:$n?"all":"none",locked:!wt&&$n,note:!wt&&$n?"matched by a pattern":wt&&!$n?"excluded by a pattern":""}},F=String(e.preserve.search||"").trim().toLowerCase();if(F)return te.files.filter(be=>be.toLowerCase().includes(F)).slice(0,300).map(be=>_(be,0,be));let Q=[],pe=(be,Xe)=>{let Yt=te.dirs.get(be);for(let wt of Yt.dirs){let $n=!!e.preserve.expanded[wt],va=Z().includes(U(wt));Q.push({kind:"dir",path:wt,label:wt.slice(be?be.length+1:0)+"/",depth:Xe,open:$n,state:R(wt),locked:!1,note:va?"whole folder":""}),$n&&pe(wt,Xe+1)}for(let wt of Yt.files)Q.push(_(wt,Xe,wt.slice(be?be.length+1:0)))};return pe("",0),Q},e.preserveCount=()=>te?te.files.filter(C).length:0,e.togglePreserveDir=S=>{e.preserve.expanded[S]=!e.preserve.expanded[S]},e.togglePreserveFile=S=>{e.preserve.picked[S]=!e.preserve.picked[S],L++},e.togglePreserveFolder=S=>{let _=U(S),F=oe(S),Q=Z().filter(be=>be!==_&&be!==F),pe=R(S)==="all"||Z().includes(_);for(let be of Oe(S))e.preserve.picked[be]=!1;L++,e.preserve.patterns=(pe?Q:[...Q,_]).join(`
|
||||
`),pe&&Oe(S).some(C)&&(e.preserve.patterns=[...Q,F].join(`
|
||||
`))};let W=[".github","","docs"].flatMap(S=>["README.md","README","README.markdown","README.rst","README.txt","readme.md"].map(_=>(S?S+"/":"")+_));e.readmePreserved=()=>{if(e.detectedType!=="repo")return!1;let S=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean);if(!S.length)return!1;try{let _=ua(S);return W.some(F=>_(F))}catch{return!1}};function z(){let S={terms:e.terms&&!e.readmePreserved()?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(S.branchName=e.source.branch);try{let _=parseGithubUrl(e.sourceUrl);S.repoName=`${_.owner}/${_.repo}`}catch{}return S}function Y(S,_,F){let Q=null,pe=0,be=()=>{if(e.detectedType!==S)return;Q&&a.cancel(Q);let Xe=++pe;e.previewStatus="loading",Q=a(()=>{Q=null;let Yt=_();if(!Yt){e.previewStatus="idle";return}t.post("/api/anonymize-preview",Yt).then(wt=>{Xe===pe&&(F(wt.data,Yt),e.previewStatus="ready")}).catch(()=>{Xe===pe&&(e.previewStatus="error")})},200)};return be.cancel=()=>{pe++,Q&&a.cancel(Q),Q=null},e.on("dispose",be.cancel),be}e.retryPreview=()=>{e.detectedType==="repo"&&V(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()};let V=Y("repo",()=>e.readme?{content:e.readme,options:z()}:null,S=>{if(typeof S?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=S.content;let _="";try{let Q=parseGithubUrl(e.sourceUrl),pe=e.source.branch||e.details&&e.details.defaultBranch||"main";_=`https://github.com/${Q.owner}/${Q.repo}/raw/${pe}/`}catch{}let F=renderMD(e.anonymize_readme,_);e.html_readme=F,a(Prism.highlightAll,150)});function j(){!e.anonymize||!e.anonymize.terms||V()}async function ee(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye();let F=await t.get(`/api/pr/${S.owner}/${S.repo}/${S.pullRequestId}`,{params:{connection:e.githubConnection}});if(!g(_))return;e.details=F.data,e.pullRequestId||(e.pullRequestId=S.repo+"-PR"+S.pullRequestId+"-"+generateRandomId(4))}catch(F){if(!g(_))return;throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}let K=et(new Map);function me(){let S=new Set,_=e.details&&e.details.pullRequest;if(!_)return S;typeof _.title=="string"&&S.add(_.title),typeof _.body=="string"&&S.add(_.body),typeof _.diff=="string"&&S.add(_.diff);let F=_.comments||[];for(let Q of F)typeof Q.author=="string"&&S.add(Q.author),typeof Q.body=="string"&&S.add(Q.body);return S}let ne=Y("pr",()=>{let S=me(),_=Array.from(S);return _.length===0?null:{contents:_,options:z()}},(S,_)=>{if(!Array.isArray(S?.contents)||S.contents.length!==_.contents.length||S.contents.some(pe=>typeof pe!="string"))throw Error("Invalid preview response");let F=_.contents,Q=new Map;for(let pe=0;pe<F.length&&pe<S.contents.length;pe++)Q.set(F[pe],S.contents[pe]);K.clear(),Q.forEach((pe,be)=>K.set(be,pe))});e.anonymizePrContent=function(S){return S&&(K.has(S)?K.get(S):"")};async function fe(){let S=parseGithubUrl(e.sourceUrl),_=m();try{Ye(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let F=await t.get(`/api/gist/source/${S.gistId}`);if(!g(_))return;e.details=F.data,e.gistId||(e.gistId="gist-"+S.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(F){if(!g(_))return;if(F.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,ae("sourceUrl","missing",!1);return}if(F.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,ae("sourceUrl","missing",!1);return}throw F.data&&(r("ERRORS."+F.data.error).then(Q=>{e.addToast({title:"Error",date:new Date,body:Q}),e.error=Q},console.error),lt(F.data.error)),ae("sourceUrl","missing",!1),F}finally{k(_)}}let ye=et(new Map);function Ae(){let S=new Set,_=e.details&&e.details.gist;if(!_)return S;typeof _.description=="string"&&S.add(_.description),typeof _.ownerLogin=="string"&&S.add(_.ownerLogin);let F=_.files||[];for(let pe of F)typeof pe.filename=="string"&&S.add(pe.filename),typeof pe.content=="string"&&S.add(pe.content);let Q=_.comments||[];for(let pe of Q)typeof pe.author=="string"&&S.add(pe.author),typeof pe.body=="string"&&S.add(pe.body);return S}let Le=Y("gist",()=>{let S=Ae(),_=Array.from(S);return _.length===0?null:{contents:_,options:z()}},(S,_)=>{if(!Array.isArray(S?.contents)||S.contents.length!==_.contents.length||S.contents.some(pe=>typeof pe!="string"))throw Error("Invalid preview response");let F=_.contents,Q=new Map;for(let pe=0;pe<F.length&&pe<S.contents.length;pe++)Q.set(F[pe],S.contents[pe]);ye.clear(),Q.forEach((pe,be)=>ye.set(be,pe)),Be()});e.anonymizeGistContent=function(S){return S&&(ye.has(S)?ye.get(S):"")},e.previewGistFiles=[];function Be(){let S=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=S.map(_=>({filename:e.anonymizeGistContent(_.filename),content:e.anonymizeGistContent(_.content),language:_.language}))}e.watch("details.gist.files",Be,!0),e.watch("terms",Be);function st(){let S=e.conference;if(e.conference_data=null,!S)return;let _=S===E||e.isUpdate&&S===e._originalConference;S!==E&&(E=null),t.get("/api/conferences/"+S).then(F=>{e.conference===S&&(e.conference_data=F.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!_&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.confere
Line truncated
|
||||
`).filter(be=>be),connection:e.githubConnection,fullName:`${F.owner}/${F.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(Q.options.pageSource=e.details.pageSource),Ye();let pe=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/status/"+e.repoId},be=>{if(be.data){let Xe=["quota_exceeded","subscription_required_for_never_expiration"].includes(be.data.error);Xe||r("ERRORS."+be.data.error).then(Yt=>{e.error=Yt},console.error),lt(be.data.error),Xe&&(be.data.error==="subscription_required_for_never_expiration"&&(e._existingNever=!1),e.hostingQuotaBlocked=!0,e.hostingReview=!0,nt(()=>document.getElementById("repository-hosting")?.scrollIntoView({block:"center"})))}}).finally(()=>{_.disabled=!1})},e.anonymizeGist=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=S.currentTarget||S.target.closest?.("button")||S.target;_.disabled=!0;let F=parseGithubUrl(e.sourceUrl),Q={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(be=>be),source:{gistId:F.gistId},options:e.options,conference:e.conference};Ye();let pe=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/gist/"+e.gistId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Xe=>{e.error=Xe},console.error),lt(be.data.error))}).finally(()=>{_.disabled=!1})},e.anonymizePullRequest=S=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(bt())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=S.currentTarget||S.target.closest?.("button")||S.target;_.disabled=!0;let F=parseGithubUrl(e.sourceUrl),Q={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(be=>be),source:{repositoryFullName:`${F.owner}/${F.repo}`,pullRequestId:F.pullRequestId},options:e.options,conference:e.conference};Ye();let pe=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(pe,Q,{headers:{"Content-Type":"application/json"}}).then(()=>{b.clear(),window.location.href="/pr/"+e.pullRequestId},be=>{be.data&&(r("ERRORS."+be.data.error).then(Xe=>{e.error=Xe},console.error),lt(be.data.error))}).finally(()=>{_.disabled=!1})},e.watch("conference",()=>{st()}),e.termHints=[],e.matchTermAsWritten=S=>{e.terms=qu(e.terms,S)},e.watch("options.preserveFiles",()=>{e.detectedType==="repo"&&V()}),e.watch("terms",()=>{e.termHints=Vu(e.terms),e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.image",()=>{e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("options.link",()=>{e.detectedType==="repo"&&j(),e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()}),e.watch("details",()=>{e.detectedType==="pr"&&ne(),e.detectedType==="gist"&&Le()},!0)},fa=function(e,t,o,i,s,r){let a=Tt();Uu();let u=bn();e.on("dark-mode",(U,oe)=>{e.aceOption&&(oe?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let p=0,v=null,h=null;e.fileLoadingSlow=!1,e.fileReceivedBytes=0;function g(){v&&(v.resolve(),v=null),h&&(a.timeout.cancel(h),h=null)}let m=!1;e.on("dispose",()=>{m=!0,p++,g(),k&&k.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"click",U=>{e.symbolsOpen&&!U.target.closest?.(".symbols")&&(e.symbolsOpen=!1),e.moreOpen&&!U.target.closest?.(".tb-more")&&(e.moreOpen=!1),e.tocOpen&&!U.target.closest?.(".tb-toc-m")&&(e.tocOpen=!1),e.gotoOpen&&U.target.isConnected&&!U.target.closest?.(".goto-pop, .btn-goto, .tb-more")&&(e.gotoOpen=!1),e.accountOpen&&!U.target.closest?.(".acct")&&(e.accountOpen=!1),e.helpOpen&&!U.target.closest?.(".vb-help")&&(e.helpOpen=!1)}),u(document,"keydown",U=>{U.key==="Escape"&&(e.symbolsOpen=!1,e.moreOpen=!1,e.tocOpen=!1,e.gotoOpen=!1,e.accountOpen=!1,e.helpOpen=!1),(U.ctrlKey||U.metaKey)&&!U.altKey&&U.key&&U.key.toLowerCase()==="f"&&e.type==="html"&&window.document.querySelector(".reading-layout")&&(U.preventDefault(),e.toggleMdFind(!0))}),u(document,"keydown",function(U){if((U.metaKey||U.ctrlKey)&&U.key==="k"){U.preventDefault();var oe=document.querySelector(".tree-search-input");oe&&(oe.focus(),oe.select())}});var k=null;e.onFileSearchChange=function(){k&&(k.resolve(),k=null);let U=e.fileSearchQuery;if(!U||U.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let oe=r.defer();k=oe,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(U)}`,{timeout:oe.promise}).then(function(Z){if(!(m||k!==oe)){k=null,e.fileSearchLoading=!1;var te={};e.files.forEach(function(j){te[(j.path||"")+"/"+j.name]=!0});for(var L=[],x={},H=0;H<Z.data.length;H++){var ue=Z.data[H];if(ue.path)for(var we=ue.path.split("/"),Oe="",C=0;C<we.length;C++){var R=Oe;Oe=Oe?Oe+"/"+we[C]:we[C];var I=R+"/"+we[C];!te[I]&&!x[I]&&(x[I]=!0,L.push({name:we[C],path:R}))}}L.sort(function(j,ee){return(j.path||"").split("/").length-(ee.path||"").split("/").length}),L.length>0&&e.files.push.apply(e.files,L);for(var W=[],z=0;z<Z.data.length;z++){var Y=Z.data[z],V=(Y.path||"")+"/"+Y.name;!te[V]&&Y.size!=null&&(W.push(Y),te[V]=!0)}W.length>0&&e.files.push.apply(e.files,W),e.fileSearchResults=Z.data}},function(){!m&&k===oe&&(k=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let T={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},w=["license","txt"],b=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],E=["wav","mp3","ogg","wma","flac","aac","m4a"],N=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(U,oe){if(e.repoId!=i.repoId)return Ze();if((i.path||"")!=e.filePath){window.matchMedia?.("(max-width: 767px)").matches&&(e.sidebarCollapsed=!0),e.filePath=i.path||"",e.paths=e.filePath.split("/").filter(Z=>Z&&Z.trim().length>0),rt();for(let Z=0;Z<e.paths.length-1;Z++){let te=Z>0?e.paths.slice(0,Z).join("/"):"";e.files.some(x=>x.path===te)||e.getFiles(te)}}});function O(){if(e.paths[0]!="")return;let U=["readme.md","readme.txt","readme.org","readme.1st","readme"],oe={};for(let te of e.files)te.name.toLowerCase().indexOf("readme")>-1&&(oe[te.name.toLowerCase()]=te.name);let Z=null;for(let te of U)if(oe[te]){Z=te;break}if(!Z&&Object.keys(oe).length>0&&(Z=Object.keys(oe)[0]),Z){let te=o.url();te[te.length-1]!="/"&&(te+="/"),o.url(te+encodePathForUrl(oe[Z]))}}e.fileCounts=null;let q=0,M=new Set,G=!1;e.getFiles=function(U,oe=!1){let Z=e.repoId,te=q;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(U)}&v=${e.options.lastUpdateDate}`).then(function(L){if(m||Z!==e.repoId||te!==q)return[];let x=U||"";return e.files=e.file
Line truncated
|
||||
`).forEach((oe,Z)=>{for(let[te,L]of he){let x=te.exec(oe);if(x&&!/^(if|for|while|switch|return|catch)$/.test(x[1])){U.push({name:x[1],kind:L,line:Z+1});break}}}),Ce={content:e.content,list:U.slice(0,300)},Ce.list},e.goToSymbol=function(U){e.symbolsOpen=!1;let oe=e.codeEditor;oe&&(oe.scrollToLine(U-1,!0,!0,()=>{}),oe.gotoLine(U,0,!0),window.history.replaceState(null,"",window.location.pathname+window.location.search+"#L"+U),window.dispatchEvent(new HashChangeEvent("hashchange")))},e.gotoLine=function(){e.codeEditor&&(e.moreOpen=!1,e.gotoError="",e.gotoValue=String(e.codeEditor.getCursorPosition().row+1),e.gotoOpen=!0,setTimeout(()=>{let U=window.document.getElementById("goto-input");U&&(U.focus(),U.select())},0))},e.closeGoto=function(){e.gotoOpen=!1,e.gotoError="",e.codeEditor?.focus()},e.submitGoto=function(){let U=e.codeEditor;if(!U)return;let oe=/^\s*:?\s*(\d+)\s*(?:[:,]\s*(\d+))?\s*$/.exec(e.gotoValue||""),Z=U.session.getLength();if(!oe||+oe[1]<1||+oe[1]>Z){e.gotoError="Enter a line from 1 to "+Z;return}U.gotoLine(+oe[1],oe[2]?Math.max(0,+oe[2]-1):0,!0),e.gotoOpen=!1,e.gotoError="",U.focus()},e.rootEntries=function(){return[...e.files||[]].sort((U,oe)=>{let Z=U.size==null,te=oe.size==null;return Z!==te?Z?-1:1:U.name.localeCompare(oe.name)})},e.formatSize=function(U){if(U==null)return"";if(U<1024)return U+" B";let oe=U/1024;return oe<1024?oe.toFixed(1)+" KB":(oe/1024).toFixed(1)+" MB"},e.fileSize=function(U){if(!U)return null;let oe=U.replace(/^\/+/,""),Z=(e.files||[]).find(te=>(te.path?te.path+"/":"")+te.name===oe);return Z&&Z.size!=null?Z.size:null},e.revealFolder=function(U){e.sidebarHidden=!1,e.sidebarCollapsed=!1,nt(()=>{let oe=window.document.querySelector('.files a[data-path="/'+(window.CSS?.escape?window.CSS.escape(U):U)+'"]');oe&&(oe.closest("li")?.classList.contains("open")||oe.click(),oe.scrollIntoView({block:"nearest"}),oe.focus())})},e.redactionCount=function(){return typeof e.content!="string"?0:((e.type==="html"?e.content.replace(/<[^>]*>/g," "):e.content).match(Os)||[]).length},e.renderRedacted=Lu;let Pe=U=>U.toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").slice(0,60)||"section";e.renderReadme=function(U){let oe=e.renderRedacted(U);if(!oe||typeof window.DOMParser!="function")return oe;let Z=new window.DOMParser().parseFromString("<div>"+oe+"</div>","text/html"),te={};return Z.querySelectorAll("h1, h2, h3").forEach(L=>{let x=L.id||Pe(L.textContent);te[x]?x+="-"+ ++te[x]:te[x]=1,L.id=x}),Z.body.firstChild.innerHTML};let Ne={content:null,list:[]};e.readmeToc=function(){if(e.type!=="html"||typeof e.content!="string")return[];if(Ne.content===e.content)return Ne.list;let oe=[...new window.DOMParser().parseFromString("<div>"+e.renderReadme(e.sanitize?e.sanitize(e.content):e.content)+"</div>","text/html").querySelectorAll("h2, h3")].map(Z=>({id:Z.id,text:Z.textContent.trim(),level:Z.tagName==="H3"?3:2})).slice(0,40);return Ne={content:e.content,list:oe},oe},e.activeHeading=null,u(window,"scroll",()=>{let U=e.readmeToc?e.readmeToc():[];if(!U.length)return;let oe=150,Z="",te="";for(let L of U){let x=window.document.getElementById(L.id);if(!x)continue;let H=x.getBoundingClientRect().top;H<oe?Z=L.id:!te&&H<window.innerHeight-80&&(te=L.id)}te&&(Z=te),e.activeHeading!==Z&&(e.activeHeading=Z)},{capture:!0,passive:!0}),e.scrollToHeading=function(U){if(!U){(window.document.querySelector(".explorer-content, .file-card-body, .main-pane")||window).scrollTo({top:0,behavior:"smooth"}),e.activeHeading="";return}let oe=window.document.getElementById(U);oe&&oe.scrollIntoView({behavior:"smooth",block:"start"}),e.activeHeading=U},e.redactParts=function(U){let oe=[],Z=0,te,L=/\bXXXX-(\d+)\b/g;for(U=String(U||"");te=L.exec(U);)te.index>Z&&oe.push({t:U.slice(Z,te.index)}),oe.push({n:te[1]}),Z=te.index+te[0].length;return Z<U.length&&oe.push({t:U.slice(Z)}),oe},e.pathCopied=!1,e.copyPath=async function(){window.navigator.clipboard?.writeText&&(await window.navigator.clipboard.writeText(e.filePath||""),e.pathCopied=!0,setTimeout(()=>{e.pathCopied=!1},1600))},e.contentCopied=!1,e.copyContent=async function(){if(typeof e.content!="string"||!window.navigator.clipboard?.writeText)return;let U=e.content;if(e.type==="html"&&e.url)try{U=await(await window.fetch(e.url,{credentials:"same-origin"})).text()}catch{return}await window.navigator.clipboard.writeText(U),e.contentCopied=!0,setTimeout(()=>{e.contentCopied=!1},1600)};function Re(U){return T[U]?T[U]:U}function Qe(U){return U=="pdf"?"pdf":U=="html"||U=="htm"?"html-doc":U=="md"?"md":U=="org"?"org":U=="ipynb"?"IPython":w.indexOf(U)>-1?"text":b.indexOf(U)>-1?"image":N.indexOf(U)>-1?"media":E.indexOf(U)>-1?"audio":"code"}function Ke(U,oe){let Z=p;if(!U){e.type="error",e.content="no_file_selected";return}let te=e.type;(te==="code"||te==="text")&&Ms().catch(()=>{}),e.type="loading",e.content="loading",e.fileLoadingSlow=!1,e.fileReceivedBytes=0;let L=r.defer();v=L,h=a.timeout(()=>{!m&&Z===p&&e.type==="loading"&&(e.fileLoadingSlow=!0)},1500);let x=oe&&oe.sha||
Line truncated
|
||||
`+w.join(`
|
||||
`)],{type:"text/csv"}),E=document.createElement("a");E.href=URL.createObjectURL(b),E.download=i.username+"-repositories.csv",E.click()},e.showStatusMessage=m=>{let k=m.statusMessage||"(no message)";window.prompt(`Status message for ${m.repoId} (${m.status}):`,k)},e.fetchGithubInfo=m=>{let k=window.open("","_blank");k&&k.document.write("<pre>Loading GitHub info for "+m.repoId+"...</pre>"),t.get("/api/admin/repos/"+m.repoId+"/github").then(T=>{k&&(k.document.open(),k.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(T.data,null,2).replace(/[<>]/g,w=>w==="<"?"<":">")+"</pre>"),k.document.close())},T=>{let w=T&&T.data?JSON.stringify(T.data,null,2):String(T);k&&(k.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+w+"</pre>")})};function p(m){t.get("/api/admin/users/"+m+"/repos",{}).then(k=>{e.repositories=k.data},k=>{console.error(k)})}function v(m){t.get("/api/admin/users/"+m,{}).then(k=>{e.userInfo=k.data},k=>{console.error(k)})}v(i.username),p(i.username),e.banUser=()=>{confirm(`Ban user ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/ban`).then(()=>v(i.username),m=>console.error(m))},e.activateUser=()=>{t.post(`/api/admin/users/${i.username}/activate`).then(()=>v(i.username),m=>console.error(m))},e.promoteUser=()=>{confirm(`Promote ${i.username} to admin?`)&&t.post(`/api/admin/users/${i.username}/promote`).then(()=>v(i.username),m=>console.error(m))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${i.username}?`)&&t.post(`/api/admin/users/${i.username}/demote`).then(()=>v(i.username),m=>console.error(m))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function h(){t.get("/api/admin/tokens").then(m=>{e.tokens=m.data||[]},m=>{m.status!==401&&m.status!==403&&console.error(m)})}h(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(m=>{e.tokenForm.plaintext=m.data.token,e.tokenForm.name="",h()},m=>console.error(m))},e.revokeToken=m=>{confirm(`Revoke token "${m.name}"?`)&&t.delete("/api/admin/tokens/"+m.id).then(()=>h(),k=>console.error(k))},e.removeCache=m=>{confirm("Remove cached files for "+m.repoId+"?")&&t.delete("/api/admin/repos/"+m.repoId).then(()=>p(i.username),k=>console.error(k))},e.removeRepository=m=>{confirm("Remove repository "+m.repoId+"?")&&t.delete("/api/repo/"+m.repoId+"/").then(()=>p(i.username),k=>console.error(k))},e.updateRepository=m=>{let k=et({title:`Refreshing ${m.repoId}...`,date:new Date,body:`The repository ${m.repoId} is going to be refreshed.`});e.toasts.push(k),t.post(`/api/repo/${m.repoId}/refresh`).then(T=>{T.data.status=="ready"?k.title=`${m.repoId} is refreshed.`:k.title=`Refreshing of ${m.repoId}.`},T=>{k.title=`Error during the refresh of ${m.repoId}.`,k.body=T.body})},e.getGitHubRepositories=m=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(k=>{e.userInfo.repositories=k.data})};let g=null;e.watch("query",()=>{s.timeout.cancel(g),g=s.timeout(()=>{p(i.username)},500)},!0)},ic=function(e,t,o){let i=Tt(),s=bn();e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let r=T=>{if(T.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){T.preventDefault();let w=document.querySelector('.admin-filter-toolbar input[type="search"]');w&&w.focus()}};s(document,"keydown",r),e.on("dispose",()=>document.removeEventListener("keydown",r)),e.clearFilter=T=>{T==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[T]="",e.query.page=1},e.chips=[];let a=()=>{let T=[];(e.query.dateFrom||e.query.dateTo)&&T.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=T};e.statusCountFor=T=>{let w=(e.statusCounts||[]).find(b=>b._id===T);return w?w.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=T=>{e.query.sort===T?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=T,e.query.direction="desc"),e.query.page=1},e.sortIcon=T=>e.query.sort===T?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",p={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!0,expired:!0,removed:!1,error:!0,preparing:!0},v=loadFilterPrefs(u)||{};e.query=Object.assign({},p,v,{page:1,search:""});let h=o.search();h.search&&(e.query.search=h.search);let g="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(g)||"[]"),e.savePreset=()=>{let T=window.prompt("Preset name:");if(!T)return;let w=Object.assign({},e.query);delete w.page,e.presets=(e.presets||[]).filter(b=>b.name!==T),e.presets.push({name:T,query:w}),localStorage.setItem(g,JSON.stringify(e.presets))},e.applyPreset=T=>{Object.assign(e.query,T.query,{page:1})},e.deletePreset=T=>{e.presets=(e.presets||[]).filter(w=>w.name!==T.name),localStorage.setItem(g,JS
Line truncated
|
||||
`).map((Pe,Ne)=>Ne===0?Pe:Ce+Pe).join(`
|
||||
`)}else typeof D=="number"||typeof D=="boolean"?he=String(D):he=JSON.stringify(D);de.push(`${Ee}${he}${ae}`)}),de.push("}"),de.join(`
|
||||
`)}function v(E){let N=[],O="",q=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,M=0,G;for(;G=q.exec(E);)O+=E.slice(M,G.index),M=q.lastIndex,N.push({key:G[1],op:G[2]||"=",val:G[3]});return O+=E.slice(M),{filters:N,free:O.trim().toLowerCase()}}function h(E,N){for(let O of N.filters){let q=(G,de,re)=>{let D=parseFloat(G),X=parseFloat(de);return re==="="?String(G)===String(de):re==="!="?String(G)!==String(de):re===">="?D>=X:re==="<="?D<=X:re===">"?D>X:re==="<"?D<X:!0},M;if(O.key==="code")M=E.displayMessage;else if(O.key==="module")M=E.module;else if(O.key==="status")M=E._status;else if(O.key==="url")M=E._url;else if(O.key==="repo")M=E._repoId;else if(O.key==="level")M=E.level;else continue;if(M==null||!q(M,O.val,O.op))return!1}return!(N.free&&((E.displayMessage||"")+" "+(E.module||"")+" "+(E._url||"")+" "+JSON.stringify(E.raw||[])).toLowerCase().indexOf(N.free)===-1)}function g(){let E=v(e.query.search||"");e.parsedFilterCount=E.filters.length;let N=e.query.bucket,O=e.entries.filter(re=>N&&re._bucket!==N?!1:h(re,E)),q=e.query.group;if(q){let re=le=>q==="module"?le.module:le.displayMessage||le.message||"_",D=new Map;for(let le of O){let Ee=re(le);if(D.has(Ee)){let ae=D.get(Ee);ae.count++,ae._related.push(le),new Date(le.ts)>new Date(ae.ts)&&(ae.ts=le.ts,ae._url=le._url,ae._status=le._status),new Date(le.ts)<new Date(ae._firstSeen)&&(ae._firstSeen=le.ts)}else{let ae=Object.assign({},le);ae._key=`${q}:${Ee}`,ae._related=[le],ae._firstSeen=le.ts,ae._lastHourCount=0,ae.count=1,D.set(Ee,ae)}}let X=Date.now()-3600*1e3;for(let le of D.values())le._lastHourCount=le._related.filter(Ee=>new Date(Ee.ts).getTime()>=X).length;O=Array.from(D.values())}else O=O.map((re,D)=>(re._key="row:"+D+":"+re.ts,re._related=[re],re._firstSeen=re.ts,re._lastHourCount=0,re.count=1,re));let M=re=>({error:0,warn:1,info:2})[re._bucket]??1;e.query.sort==="count"?O.sort((re,D)=>M(re)-M(D)||D.count-re.count||new Date(D.ts)-new Date(re.ts)):O.sort((re,D)=>M(re)-M(D)||new Date(D.ts)-new Date(re.ts)),e.visible=O;let G=!!(e.query.bucket||(e.query.search||"").trim()),de=!G&&(e.stats?.severity?.error||0)>0&&!O.some(re=>re._bucket==="error")&&e.entries.length<1500;(G&&!O.length||de)&&e.canLoadMore?.()&&!e._autoLoading&&e.entries.length<2500&&(e._autoLoading=!0,m(!0))}function m(E){let N=E?e.entries.length:0,O=E?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:N,limit:O}}).then(q=>{let M=(q.data.entries||[]).map(u);e.entries=E?e.entries.concat(M):M,e.available=!!q.data.available,e.cap=q.data.max||e.cap,e.total=q.data.total||e.entries.length,e._autoLoading=!1,g()},q=>{e._autoLoading=!1,console.error(q)})}e.loadMore=()=>m(!0),e.canLoadMore=()=>e.entries.length<e.total;function k(){t.get("/api/admin/errors/stats").then(E=>{let N=E.data||{},O=N.prev24h?Math.round((N.last24h-N.prev24h)/N.prev24h*100):0;e.stats={last24h:N.last24h||0,prev24h:N.prev24h||0,delta:O,severity:N.severity||{error:0,warn:0,info:0},unique:N.unique||{error:0,warn:0,info:0},buckets:N.buckets||[],dropped:N.dropped||0},g()},E=>console.error(E))}function T(){m(),k()}e.barPx=(E,N)=>{let O=e.stats.buckets||[],q=0,M=0;for(let D of O)q=Math.max(q,(D.error||0)+(D.warn||0)),M=Math.max(M,D.info||0);let G=E[N]||0;if(!G)return 0;if(N==="info")return M?Math.max(2,Math.round(G/M*(q?16:60))):0;let de=(E.error||0)+(E.warn||0),re=Math.round(de/q*44);return Math.max(4,Math.round(G/de*re))},e.bucketTitle=E=>`${new Date(E.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${E.error||0} err \xB7 ${E.warn||0} warn \xB7 ${E.info||0} info`,e.toggle=E=>{e.expanded[E._key]=!e.expanded[E._key]},e.setBucket=E=>{e.query.bucket=E},e.refreshNow=T,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(T,E=>console.error(E))},e.exportCsv=()=>{let E=["ts","level","module","displayMessage","_status","_url","_repoId"],N=[E.join(",")];for(let G of e.visible)N.push(E.map(de=>{let re=G[de]==null?"":String(G[de]);return/[",\n]/.test(re)?`"${re.replace(/"/g,'""')}"`:re}).join(","));let O=new Blob([N.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),q=URL.createObjectURL(O),M=document.createElement("a");M.href=q,M.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(M),M.click(),document.body.removeChild(M),URL.revokeObjectURL(q)};function w(E){e.copyHint=`${E} copied`,s.timeout(()=>{e.copyHint=""},1500)}e.copyJson=E=>{navigator.clipboard.writeText(E._detailJson).then(()=>w("JSON"))},e.copyCurl=E=>{if(!E._url)return;let O=`curl -X ${E._method||"GET"} '${window.location.origin}${E._url}'`;navigator.clipboard.writeText(O).then(()=>w("curl"))},T();let b=i(()=>{e.query.autoRefresh&&T()},15e3);e.on("dispose",()=>i.cancel(b)),e.watch("query.search",g),e.watch("query.bucket",g),e.watch("query.sort",g),e.watch("query.group",g)},lc=function(e,t,o,i){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceTab="routes",e.cacheReport=null,e.cacheError=null,e.cacheMinutes=60,e.cacheTraffic="all",e.ovView=["performance","cache"].includes((window.location.hash||"").slice(1))?window.location.hash.slice(1):"summary",e.setOvView=function(b){e.ovView=b;try{window.history.replaceState(null,"",b==="summary"?window.location.pathname:"#"+b)}catch{}};var s=!1;e.loadCache=function(){if(!s){s=!0;var b=e.cacheMinutes;t.get("/api/admin/cache",{params:{minutes:b}}).then(function(E){b===e.cacheMinutes&&(e.cacheReport=E.data,e.cacheError=E.data.available?null:E.data.reason||"Cache monitoring is unavailable.")},function(){e.cacheError="Could not refresh cache monitoring. Displayed values may be stale."}).finally(function(){s=!1,b!==e.cacheMinutes&&e.loadCache()})}},e.cacheRows=function(){return(e.cacheReport?.rows||[]).filter(function(b){return e.cacheTraffic==="all"||b.traffic===e.cacheTraffic})},e.cacheRate=function(b,E){var N=e.cacheRows(),O=function(G){return N.filter(function(de){return de.name===G}).reduce(function(de,re){return de+re[E]},0)},q=O(b+"_hit"),M=O(b+"_miss");return q+M?(100*q/(q+M)).toFixed(1)+"%":"No samples"},e.cacheEventName=function(b){return b.replaceAll("_"," ")},e.cacheP95=function(b){var E=0,N=e.cacheReport?.latencyBucketsMs||[];if(!b.count)return"No samples";if(!b.ms)return"Not timed";for(var O=0;O<=N.length;O++)if(E+=b["b"+O]||0,E>=Math.ceil(b.count*.95))return O<N.length?"\u2264 "+N[O]+" ms":"> 10 s";return"No samples"},e.performanceError=null,e.formatMs=function(b){return b==null?"":b<1e3?b+" ms":+(b/1e3).toFixed(1)+" s"},e.latencyBound=function(b,E){return b.count?b[E]==null?"> 60 s":"\u2264 "+e.formatMs(b[E]):"No samples"},e.firstByteBound=function(b){return e.latencyBound({count:b.firstByteCount,p95UpperMs:b.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(b){return Math.max(0,Math.round((Date.now()-b.sampledAt)/1e3))},e.peakRss=function(b){return(e.performance.runtimeSeries||[]).filter(function(E){return E.instance===b.instance}).reduce(function(E,N){return Math.max(E,N.rss)},b.memory.rss)},e.performanceWarning=function(b){return e.sampleAge(b)>45||b.droppedBatches||b.droppedMetrics||b.sockets?.closeWait>0||b.eventLoop?.p95Ms>100||b.memoryLimitBytes&&b.memory.rss>b.memoryLimitBytes*.8};var r=!1;e.loadPerformance=function(){var b=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==b&&(e.performance=null),!r&&(r=!0,t.get("/api/admin/performance",{params:{minutes:b}}).then(function(E){b===e.performanceMinutes&&(e.performance=E.data,e.performanceError=E.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){b===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){r=!1,b!==e.performanceMinutes&&e.loadPerformance()}))};function a(b){if(b==null)return"\u2014";for(var E=["B","KB","MB","GB","TB"],N=0,O=b;O>=1024&&N<E.length-1;)O/=1024,N++;return O.toFixed(N>0?1:0)+" "+E[N]}e.humanBytes=a;function u(b){if(!b)return"\u2014";var E=Math.floor(b/86400),N=Math.floor(b%86400/3600),O=Math.floor(b%3600/60);return E>0?E+"d "+(N<10?"0":"")+N+"h":N>0?N+"h "+(O<10?"0":"")+O+"m":O+"m"}e.humanDuration=u;function p(b){return b==null?"\u2014":b>=1e6?(b/1e6).toFixed(1)+"M":b>=1e3?(b/1e3).toFixed(1)+"K":String(b)}e.humanNum=p,e.queueTotal=function(b){return b?(b.waiting||0)+(b.active||0)+(b.delayed||0)+(b.failed||0):0},e.statusCount=function(b){if(!e.data||!e.data.repos)return 0;for(var E=e.data.repos.statusBreakdown||[],N=0;N<E.length;N++)if(E[N]._id===b)return E[N].count;return 0},e.barPct=function(b){var E=e.data&&e.data.repos?e.data.repos.total:0;if(!E)return 0;var N=[b];b==="expired"&&N.push("expiring"),b==="removed"&&N.push("removing"),b==="preparing"&&N.push("download");var O=0;return N.forEach(function(q){O+=e.statusCount(q)}),Math.max(.4,O/E*100)},e.errPct=function(b){if(!e.data||!e.data.errors)return 0;var E=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.erro
Line truncated
|
||||
Contact: jane.smith@mit.edu`,e.demoTerms="Jane Smith, Smith, MIT CSAIL, jane.smith@mit.edu",e.demoParts=function(){let u=(e.demoTerms||"").split(/[,\n]/).map(h=>h.trim()).filter(Boolean).sort((h,w)=>w.length-h.length),p=h=>h.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(!u.length)return[{text:e.demoText}];let y=new RegExp("("+u.map(p).join("|")+")","gi"),m={},b=0;return(e.demoText||"").split(y).filter(h=>h!=="").map(h=>{let w=u.find(A=>A.toLowerCase()===h.toLowerCase());return w?(m[w.toLowerCase()]||(m[w.toLowerCase()]=++b),{text:String(m[w.toLowerCase()]),redacted:!0,original:h}):{text:h}})},e.demoCount=()=>e.demoParts().filter(u=>u.redacted).length,e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}],e.statsEmpty=()=>!!e.stat&&e.cards.every(u=>!u.total);function r(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests}).catch(()=>{})}r();function a(u){let p={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return p;let y=new Array(u.length-1);for(let N=1;N<u.length;N++)y[N-1]=u[N]-u[N-1];let m=y.length,b=Math.max.apply(null,y),h=Math.min.apply(null,y),w=Math.min(0,h),A=b-w||1;p.viewW=m*2,p.bars=new Array(m);for(let N=0;N<m;N++){let R=(y[N]-w)/A,v=Math.max(1.5,R*34);p.bars[N]={x:(N*2+.25).toFixed(2),y:(36-v).toFixed(2),w:"1.5",h:v.toFixed(2)}}if(p.deltaToday=y[m-1],m>=2){let N=y[m-2];N&&(p.pctChange=(p.deltaToday-N)/N*100)}return p.pctAbs=Math.round(Math.abs(p.pctChange)),p.isUp=p.pctChange>=0,p}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let p=u.data||[];e.history={repositories:a(p.map(y=>y.nbRepositories||0)),users:a(p.map(y=>y.nbUsers||0)),pageViews:a(p.map(y=>y.nbPageViews||0)),pullRequests:a(p.map(y=>y.nbPullRequests||0))}}).catch(()=>{})},bc=function(e,t,o,i,s,r){let a=Tt();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,ot(()=>document.querySelector(".dashboard-quota > summary")?.focus())},dn()(document,"click",D=>{e.quotaExpanded&&!D.target.closest?.(".dashboard-quota")&&(e.quotaExpanded=!1)}),e.filtersExpanded=!1,e.projectView="all",e.selectedProject=null,e.projectName="",e.nameSaving=!1,e.nameError="",e.nameMessage="",e.needsAttention=D=>D.status==="error"||D._broken||p(D.status)==="progress"&&D._stale,e.attentionCount=()=>e.dashboardTotals?.attention??e.items.filter(e.needsAttention).length,e.hasProjects=()=>(e.dashboardTotals?.total??e.items.length)>0,e.setProjectView=D=>{e.projectView=D,D==="attention"&&(e.filters.status.error=!0,e.filters.status.progress=!0)},e.idleText=D=>{let oe=Kr(D?.anonymizeDate||D?.lastView)||"";return/ ago$/.test(oe)?"No activity for "+oe.replace(/ ago$/,""):"No activity since "+oe.replace(", "+new Date().getFullYear(),"")},e.projectDetailsId=D=>"project-details-"+encodeURIComponent(D._type+":"+(D._id||D._source||"unknown")),e.showProjectDetails=async D=>{e.selectedProject=D,e.projectName=D.projectName||"",e.nameError="",e.nameMessage="",await ot();let oe=document.getElementById(e.projectDetailsId(D));oe?.focus(),oe?.scrollIntoView?.({block:"nearest"})},e.closeProjectDetails=D=>{e.selectedProject=null,ot(()=>document.querySelector(`[data-project-actions="${encodeURIComponent(D._type+":"+D._id)}"]`)?.focus())},e.saveProjectName=async D=>{if(e.nameSaving||!D._id)return;let oe=e.projectName.trim();if(oe.length>100){e.nameError="Use 100 characters or fewer.";return}e.nameSaving=!0,e.nameError="",e.nameMessage="";try{let x=await t.post("/api/user/project-name",{type:D._type,id:D._id,name:oe});D.projectName=x.data.name,D._label=D.projectName||D._fallbackLabel,e.selectedProject===D&&(e.nameMessage=oe?"Project name saved.":"Default project name restored."),C||(e.dashboardCursor=null,await V(),e.selectedProject===D&&(e.selectedProject=e.items.find(Ae=>Ae._type===D._type&&Ae._id===D._id)||null))}catch{e.selectedProject===D&&(e.nameError="The project name could not be saved. Try again.")}finally{e.nameSaving=!1}},e.copyingId=null,e.copyLink=async D=>{if(!D._viewUrl||e.copyingId)return;let oe=new URL(D._viewUrl,s.location.origin).href;e.copyingId=D._type+":"+D._id;try{await Ea(oe),e.addToast({title:"Link copied",body:"The anonymous link is ready to share."})}catch{e.addToast({title:"Copy this anonymous link",body:oe})}finally{e.copyingId=null}},e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"Preparing",error:"Error",expired:"Expiring / expired",removed:"Removing / removed"};let u=["queue","download","downloaded","preparing","anonym
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,U.options),e.options.expirationDate=U.options&&U.options.expirationDate?new Date(U.options.expirationDate):W(),!i.repoId&&!i.pullRequestId&&!i.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),k&&k()})}function x(k,_,U){e.anonymize&&e.anonymize[k]&&e.anonymize[k].setValidity(_,U)}function Ae(k){try{let _=parseGithubUrl(k);if(_&&_.owner&&_.repo)return _.owner+"/"+_.repo}catch{}return null}e.accessRepairAllowed=!1,e.repairingGitHubAccess=!1,e.accessRepairMessage="",e.repairGitHubAccess=async()=>{if(!e.accessRepairAllowed||e.repairingGitHubAccess||!e.githubConnections?.appEnabled||!i.repoId||e.role!=="owner"||e.githubConnection!=="github-app"||Ae(e.sourceUrl)!==e._originalFullName)return;e.repairingGitHubAccess=!0,e.accessRepairMessage="";let k=h();try{let _=await t.get("/github/connections");if(e.githubConnections=_.data,!_.data.appEnabled)return;let U=await t.post("/github/connections/migrate",{type:"repository",id:i.repoId,connection:"github-app"},{headers:{"X-CSRF-Token":_.data.csrf}});if(!b(k))return;e.error=null,U.data.fullName&&(e._originalFullName=U.data.fullName,e.source.fullName=U.data.fullName,e.sourceUrl="https://github.com/"+U.data.fullName),e.accessRepairMessage="GitHub access repaired. Your anonymous URL and published commit are unchanged.",await e.urlSelected(!0)}catch{b(k)&&(e.accessRepairMessage="Unable to repair GitHub access. Check the App's repository permissions in Manage access, then retry. Your published snapshot has not been replaced.")}finally{e.repairingGitHubAccess=!1,w(k)}},e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await ke()}catch{}}};function be(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:Ae(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function le(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function fe(){let k=new URLSearchParams(window.location.search).get("billingDraft");if(k){R.restore();try{let U=(await t.get("/api/billing/draft/"+encodeURIComponent(k))).data;if(U.path!==s.path())throw new Error("Wrong task");v=U.values.conference||null;for(let j of["sourceUrl","terms","repoId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(U.values,j)&&(e[j]=U.values[j]);return e.options?.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),C=U.id,I=JSON.stringify(R.checkpoint()),e.draftRestored=!0,e.hostingReview=!0,e.sourceUrl&&await le(),s.replace?.(s.path()),!0}catch{e.billingError="The checkout draft could not be restored. Retry by reloading this page; any local draft is still available."}}let _;try{_=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!_||_.path!==s.path()||Date.now()-_.savedAt>=30*6e4)return R.restore()?(e.sourceUrl&&await le(),!0):!1;R.restore();for(let U of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(_.draft,U)&&(e[U]=_.draft[U]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await le(),sessionStorage.removeItem("github-access-draft"),!0}oe(()=>{!i.repoId&&!i.pullRequestId&&!i.gistId&&a(fe,0),i.repoId&&i.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=i.repoId,t.get("/api/repo/"+e.repoId).then(async k=>{e.accessRepairAllowed=!["removed","archived","preparing","queue","download","removing","expiring"].includes(k.data.status),e.githubConnection=k.data.connection||"oauth",e.sourceUrl="https://github.com/"+k.data.source.fullName,e._originalFullName=k.data.source.fullName,e.terms=k.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=k.data.source,e.role=k.data.role||"owner",e._existingNever=k.data.options.expirationMode==="never",e._savedExpirationDate=k.data.options.expirationDate?new Date(k.data.options.expirationDate):null,e.coauthors=k.data.coauthors||[],e._originalBranch=k.data.source.branch,e.options=Object.assign({},e.options,k.data.options),e.conference=k.data.conference,e._originalConference=k.data.conference,e.repositoryID=k.data.source.repositoryID,e._originalRepositoryID=k.data.source.repositoryID,k.data.options.expirationDate&&(e.options.expirationDate=new Date(k.data.options.expirationDate)),A=N(parseGithubUrl(e.sourceUrl)),!await fe()&&(await Promise.all([ke(),Te()]),J())},()=>{s.url("/404")})),i.pullRequestId&&i.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=i.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async k=>{if(e.githubConnection=k.data.connection||"oauth",e.sourceUrl="https://github.com/"+k.data.source.repositoryFullName+"/pull/"+k.data.source.pullRequestId,e.terms=k.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=k.data.source,e.options=Object.assign({},e.options,k.data.options),e.conference=k.data.conference,e._originalConference=k.data.conference,k.data.options.expirationDate&&(e.options.expirationDate=new Date(k.data.options.expirationDate)),A=N(parseGithubUrl(e.sourceUrl)),!await fe())try{await Z()}catch{}},()=>{s.url("/404")})),i.gistId&&i.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=i.gistId,t.get("/api/gist/"+e.gistId).then(async k=>{e.sourceUrl="https://gist.github.com/"+k.data.source.gistId,e.terms=k.data.options.terms.filter(_=>_).join(`
|
||||
`),e.source=k.data.source,e.options=Object.assign({},e.options,k.data.options),e.conference=k.data.conference,e._originalConference=k.data.conference,k.data.options.expirationDate&&(e.options.expirationDate=new Date(k.data.options.expirationDate)),A=N(parseGithubUrl(e.sourceUrl)),!await fe()&&await de()},()=>{s.url("/404")}))}),t.get("/github/connections").then(k=>{e.githubConnections=k.data,!i.repoId&&!i.pullRequestId&&!i.gistId&&e.githubConnection===void 0&&k.data.appEnabled&&k.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&le())}).catch(()=>{}),e.urlSelected=async(k=!1)=>{let _=++p;e.previousConfigurations=[],e.previousConfigurationId="";let U;try{U=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,te.cancel(),ae.cancel(),Re.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",x("sourceUrl","github",!1);return}let j=N(U),ue=k||A===j;A=j,e.sourceUrlError="",x("sourceUrl","github",!0);let we=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,setTimeout(()=>{let Ze=window.document.getElementById("sourceUrl");if(Ze){try{Ze.setSelectionRange(0,0)}catch{}Ze.scrollLeft=0}},50),we&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!ue&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],ue||(e.source={type:"GitHubStream",branch:"",commit:""}),te.cancel(),ae.cancel(),Re.cancel(),G.clear(),pe.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=ue;try{U.gistId&&!U.repo?(e.detectedType="gist",e.source={gistId:U.gistId},await de()):U.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:U.owner+"/"+U.repo,pullRequestId:U.pullRequestId},await Z()):(e.detectedType="repo",D(),await Promise.all([ke(),Te()]),J())}catch{}finally{_===p&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let k=e.branches.filter(U=>U.name==e.source.branch)[0];if(!k)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=k.commit),e.readme=k.readme,await Te(),J()}),e.getBranches=async k=>{let _=parseGithubUrl(e.sourceUrl),U=h();try{let j=await t.get(`/api/repo/${_.owner}/${_.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&Ae(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:k===!0||e.reconnectingSource?"1":"0",repositoryID:be()}});if(!b(U))return;e.branches=j.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let ue=e.branches.filter(we=>we.name==e.source.branch);ue.length>0&&(!(!e.reconnectingSource&&!k&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=ue[0].commit),e.readme=ue[0].readme,await Te(k))}catch(j){if(!b(U))return;e.branches=[],e.sourceUnreachable=j&&(j.status===404||j.data&&j.data.error==="repo_not_found");let ue=j&&j.data&&j.data.error||(j&&j.status===404?"repo_not_found":"unknown_error");r("ERRORS."+ue).then(we=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:we}),e.error=we},console.error),typeof x=="function"&&x("sourceUrl","missing",!1)}finally{w(U)}};async function ke(){let k=parseGithubUrl(e.sourceUrl),_=h();try{Se();let U=await t.get(`/api/repo/${k.owner}/${k.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&Ae(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,repositoryID:be(),force:"1"}});if(!b(_))return;e.details=U.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(U){if(!b(_))return;throw U.data&&(r("ERRORS."+U.data.error).then(j=>{e.addToast({title:"Error",date:new Date,body:j}),e.error=j},console.error),Fe(U.data.error)),x("sourceUrl","missing",!1),U}finally{w(_)}}async function Te(k){if(e.readme&&!k)return e.readme;let _=parseGithubUrl(e.sourceUrl),U=h();try{let j=await t.get(`/api/repo/${_.owner}/${_.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&i.repoId&&Ae(e.sourceUrl)===e._originalFullName?i.repoId:void 0,connection:e.githubConnection,force:k===!0?"1":"0",branch:e.source.branch,repositoryID:be()}});b(U,!0)&&(e.readme=j.data)}catch{b(U,!0)&&(e.readme="")}finally{w(U)}}let Xe={magicalBraces:!0},rt=k=>k.startsWith("/")&&!k.endsWith("/")&&In(ro(k.slice(1),Xe),Xe)===k.slice(1),at=()=>Object.keys(e.preserve.picked).filter(k=>e.prese
Line truncated
|
||||
`).map(k=>k.trim()).filter(Boolean),We=null,X=0,he=null,M=()=>!1;function F(){let k=X+"\0"+e.preserve.patterns;if(k!==he){he=k;try{M=wa(ba([...at(),...et()]))}catch{M=()=>!1}}return M}function B(k){let _=new Map([["",{dirs:[],files:[]}]]);for(let U of k){let j=U.split("/"),ue="";for(let we=0;we<j.length-1;we++){let Ze=j.slice(0,we+1).join("/");_.has(Ze)||(_.set(Ze,{dirs:[],files:[]}),_.get(ue).dirs.push(Ze)),ue=Ze}_.get(ue).files.push(U)}for(let U of _.values())U.dirs.sort();return{files:k,dirs:_,descendants:new Map}}function K(k){return We.descendants.has(k)||We.descendants.set(k,We.files.filter(_=>_.startsWith(k+"/"))),We.descendants.get(k)}let E=k=>F()(k);function S(k){let _=K(k),U=0;for(let j of _)E(j)&&U++;return U===0?"none":U===_.length?"all":"some"}e.preserve={open:!1,loading:!1,error:"",truncated:!1,commit:"",search:"",expanded:{},picked:{},patterns:"",patternsOpen:!1},e.preserveSummary=()=>{let k=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean).map(_=>rt(_)?ro(_.slice(1),Xe):_);return k.length?k.slice(0,2).join(", ")+(k.length>2?` and ${k.length-2} more`:""):"None. Every file is redacted."};async function O(){let k=e.source?.commit;if(!(We&&e.preserve.commit===k)){e.preserve.loading=!0,e.preserve.error="",We=null;try{let _=parseGithubUrl(e.sourceUrl),U=await t.get(`/api/repo/${_.owner}/${_.repo}/tree`,{params:{commit:k,connection:e.githubConnection,anonymizedRepoId:e.isUpdate&&i.repoId&&Ae(e.sourceUrl)===e._originalFullName?i.repoId:void 0,repositoryID:be()}});if(e.source?.commit!==k)return;We=bo(B(U.data.files||[])),e.preserve.commit=k,e.preserve.truncated=!!U.data.truncated}catch{e.preserve.error="The file list could not be loaded from GitHub. You can still use patterns.",e.preserve.patternsOpen=!0}finally{e.preserve.loading=!1}}}e.watch("source.commit",k=>{e.detectedType==="repo"&&k&&e.sourceUrl&&O().catch(()=>{})}),e.openPreserveFiles=()=>{let k={},_=[];for(let j of e.options?.preserveFiles||[]){let ue=String(j).trim();ue&&(!_.length&&rt(ue)?k[ro(ue.slice(1),Xe)]=!0:_.push(ue))}let U={};for(let j of Object.keys(k)){let ue=j.split("/");for(let we=1;we<ue.length;we++)U[ue.slice(0,we).join("/")]=!0}Object.assign(e.preserve,{open:!0,search:"",picked:k,expanded:U,patterns:_.join(`
|
||||
`),patternsOpen:_.length>0}),X++,a(()=>{let j=document.getElementById("preserve-files-dialog");j?.showModal&&!j.open&&j.showModal(),document.getElementById("preserve-files-search")?.focus()},0),O()},e.closePreserveFiles=()=>{document.getElementById("preserve-files-dialog")?.close?.(),e.preserve.open=!1},e.savePreserveFiles=()=>{e.options.preserveFiles=ba([...at(),...et()]),e.closePreserveFiles()},e.preserveRows=()=>{if(!We)return[];let k=F(),_=(we,Ze,Yt)=>{let kt=!!e.preserve.picked[we],$n=k(we);return{kind:"file",path:we,label:Yt,depth:Ze,state:$n?"all":"none",locked:!kt&&$n,note:!kt&&$n?"matched by a pattern":kt&&!$n?"excluded by a pattern":""}},U=String(e.preserve.search||"").trim().toLowerCase();if(U)return We.files.filter(we=>we.toLowerCase().includes(U)).slice(0,300).map(we=>_(we,0,we));let j=[],ue=(we,Ze)=>{let Yt=We.dirs.get(we);for(let kt of Yt.dirs){let $n=!!e.preserve.expanded[kt],Da=et().includes(Qe(kt));j.push({kind:"dir",path:kt,label:kt.slice(we?we.length+1:0)+"/",depth:Ze,open:$n,state:S(kt),locked:!1,note:Da?"whole folder":""}),$n&&ue(kt,Ze+1)}for(let kt of Yt.files)j.push(_(kt,Ze,kt.slice(we?we.length+1:0)))};return ue("",0),j},e.preserveCount=()=>We?We.files.filter(E).length:0,e.togglePreserveDir=k=>{e.preserve.expanded[k]=!e.preserve.expanded[k]},e.togglePreserveFile=k=>{e.preserve.picked[k]=!e.preserve.picked[k],X++},e.togglePreserveFolder=k=>{let _=Qe(k),U=wt(k),j=et().filter(we=>we!==_&&we!==U),ue=S(k)==="all"||et().includes(_);for(let we of K(k))e.preserve.picked[we]=!1;X++,e.preserve.patterns=(ue?j:[...j,_]).join(`
|
||||
`),ue&&K(k).some(E)&&(e.preserve.patterns=[...j,U].join(`
|
||||
`))};let H=[".github","","docs"].flatMap(k=>["README.md","README","README.markdown","README.rst","README.txt","readme.md"].map(_=>(k?k+"/":"")+_));e.readmePreserved=()=>{if(e.detectedType!=="repo")return!1;let k=(e.options?.preserveFiles||[]).map(_=>String(_).trim()).filter(Boolean);if(!k.length)return!1;try{let _=wa(k);return H.some(U=>_(U))}catch{return!1}};function z(){let k={terms:e.terms&&!e.readmePreserved()?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(k.branchName=e.source.branch);try{let _=parseGithubUrl(e.sourceUrl);k.repoName=`${_.owner}/${_.repo}`}catch{}return k}function Y(k,_,U){let j=null,ue=0,we=()=>{if(e.detectedType!==k)return;j&&a.cancel(j);let Ze=++ue;e.previewStatus="loading",j=a(()=>{j=null;let Yt=_();if(!Yt){e.previewStatus="idle";return}t.post("/api/anonymize-preview",Yt).then(kt=>{Ze===ue&&(U(kt.data,Yt),e.previewStatus="ready")}).catch(()=>{Ze===ue&&(e.previewStatus="error")})},200)};return we.cancel=()=>{ue++,j&&a.cancel(j),j=null},e.on("dispose",we.cancel),we}e.retryPreview=()=>{e.detectedType==="repo"&&te(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&Re()};let te=Y("repo",()=>e.readme?{content:e.readme,options:z()}:null,k=>{if(typeof k?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=k.content;let _="";try{let j=parseGithubUrl(e.sourceUrl),ue=e.source.branch||e.details&&e.details.defaultBranch||"main";_=`https://github.com/${j.owner}/${j.repo}/raw/${ue}/`}catch{}let U=renderMD(e.anonymize_readme,_);e.html_readme=U,a(()=>{Prism.highlightAll(),typeof bs=="function"&&document.querySelectorAll(".reviewer-preview .anonymize-preview-body").forEach(bs)},150)});function J(){!e.anonymize||!e.anonymize.terms||te()}async function Z(){let k=parseGithubUrl(e.sourceUrl),_=h();try{Se();let U=await t.get(`/api/pr/${k.owner}/${k.repo}/${k.pullRequestId}`,{params:{connection:e.githubConnection}});if(!b(_))return;e.details=U.data,e.pullRequestId||(e.pullRequestId=k.repo+"-PR"+k.pullRequestId+"-"+generateRandomId(4))}catch(U){if(!b(_))return;throw U.data&&(r("ERRORS."+U.data.error).then(j=>{e.addToast({title:"Error",date:new Date,body:j}),e.error=j},console.error),Fe(U.data.error)),x("sourceUrl","missing",!1),U}finally{w(_)}}let G=nt(new Map);function ge(){let k=new Set,_=e.details&&e.details.pullRequest;if(!_)return k;typeof _.title=="string"&&k.add(_.title),typeof _.body=="string"&&k.add(_.body),typeof _.diff=="string"&&k.add(_.diff);let U=_.comments||[];for(let j of U)typeof j.author=="string"&&k.add(j.author),typeof j.body=="string"&&k.add(j.body);return k}let ae=Y("pr",()=>{let k=ge(),_=Array.from(k);return _.length===0?null:{contents:_,options:z()}},(k,_)=>{if(!Array.isArray(k?.contents)||k.contents.length!==_.contents.length||k.contents.some(ue=>typeof ue!="string"))throw Error("Invalid preview response");let U=_.contents,j=new Map;for(let ue=0;ue<U.length&&ue<k.contents.length;ue++)j.set(U[ue],k.contents[ue]);G.clear(),j.forEach((ue,we)=>G.set(we,ue))});e.anonymizePrContent=function(k){return k&&(G.has(k)?G.get(k):"")};async function de(){let k=parseGithubUrl(e.sourceUrl),_=h();try{Se(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let U=await t.get(`/api/gist/source/${k.gistId}`);if(!b(_))return;e.details=U.data,e.gistId||(e.gistId="gist-"+k.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(U){if(!b(_))return;if(U.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,x("sourceUrl","missing",!1);return}if(U.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,x("sourceUrl","missing",!1);return}throw U.data&&(r("ERRORS."+U.data.error).then(j=>{e.addToast({title:"Error",date:new Date,body:j}),e.error=j},console.error),Fe(U.data.error)),x("sourceUrl","missing",!1),U}finally{w(_)}}let pe=nt(new Map);function De(){let k=new Set,_=e.details&&e.details.gist;if(!_)return k;typeof _.description=="string"&&k.add(_.description),typeof _.ownerLogin=="string"&&k.add(_.ownerLogin);let U=_.files||[];for(let ue of U)typeof ue.filename=="string"&&k.add(ue.filename),typeof ue.content=="string"&&k.add(ue.content);let j=_.comments||[];for(let ue of j)typeof ue.author=="string"&&k.add(ue.author),typeof ue.body=="string"&&k.add(ue.body);return k}let Re=Y("gist",()=>{let k=De(),_=Array.from(k);return _.length===0?null:{contents:_,options:z()}},(k,_)=>{if(!Array.isArray(k?.contents)||k.contents.length!==_.contents.length||k.contents.some(ue=>typeof ue!="string"))throw Error("Invalid preview response");let U=_.contents,j=new Map;for(let ue=0;ue<U.length&&ue<k.contents.length;ue++)j.set(U[ue],k.contents[ue]);pe.clear(),j.forEach((ue,we)=>pe.set(we,ue)),_e()});e.anonymizeGistContent=function(k){return k&&(pe.has(k)?pe.get(k):"")},e.previewGistFiles=[];function _e(){let k=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=k.map(_=>({filename:e.anonymizeGistContent(_.filename),content:e.anonymizeGistContent(_.content),language:_.language}))}e.watch("details.gist.files",_e,!0),e.watch("terms",_e);function me(){let k=e.conference;if(e.conference_data=null,!k)return;let _=k===v||e.isUpdate&&k===e._originalConference;k!==v&&(v=null),t.get("/api/conferences/"+k).then(U=>{e.conference===k&&(e.conference_data=U.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!_&&(e.options.expirationDate=new Date(e.conference_data.endDat
Line truncated
|
||||
`).filter(we=>we),connection:e.githubConnection,fullName:`${U.owner}/${U.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(j.options.pageSource=e.details.pageSource),Se();let ue=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(ue,j,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/status/"+e.repoId},we=>{if(we.data){let Ze=["quota_exceeded","subscription_required_for_never_expiration"].includes(we.data.error);Ze||r("ERRORS."+we.data.error).then(Yt=>{e.error=Yt},console.error),Fe(we.data.error),Ze&&(we.data.error==="subscription_required_for_never_expiration"&&(e._existingNever=!1),e.hostingQuotaBlocked=!0,e.hostingReview=!0,ot(()=>document.getElementById("repository-hosting")?.scrollIntoView({block:"center"})))}}).finally(()=>{_.disabled=!1})},e.anonymizeGist=k=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ze())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=k.currentTarget||k.target.closest?.("button")||k.target;_.disabled=!0;let U=parseGithubUrl(e.sourceUrl),j={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(we=>we),source:{gistId:U.gistId},options:e.options,conference:e.conference};Se();let ue=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(ue,j,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/gist/"+e.gistId},we=>{we.data&&(r("ERRORS."+we.data.error).then(Ze=>{e.error=Ze},console.error),Fe(we.data.error))}).finally(()=>{_.disabled=!1})},e.anonymizePullRequest=k=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ze())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let _=k.currentTarget||k.target.closest?.("button")||k.target;_.disabled=!0;let U=parseGithubUrl(e.sourceUrl),j={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(we=>we),source:{repositoryFullName:`${U.owner}/${U.repo}`,pullRequestId:U.pullRequestId},options:e.options,conference:e.conference};Se();let ue=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(ue,j,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/pr/"+e.pullRequestId},we=>{we.data&&(r("ERRORS."+we.data.error).then(Ze=>{e.error=Ze},console.error),Fe(we.data.error))}).finally(()=>{_.disabled=!1})},e.watch("conference",()=>{me()}),e.termHints=[],e.matchTermAsWritten=k=>{e.terms=uc(e.terms,k)},e.watch("options.preserveFiles",()=>{e.detectedType==="repo"&&te()}),e.watch("terms",()=>{e.termHints=dc(e.terms),e.detectedType==="repo"&&J(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&Re()}),e.watch("options.image",()=>{e.detectedType==="repo"&&J(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&Re()}),e.watch("options.link",()=>{e.detectedType==="repo"&&J(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&Re()}),e.watch("details",()=>{e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&Re()},!0)},Sa=function(e,t,o,i,s,r){let a=Tt();pc();let u=dn();e.on("dark-mode",(M,F)=>{e.aceOption&&(F?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let p=0,y=null,m=null;e.fileLoadingSlow=!1,e.fileReceivedBytes=0;function b(){y&&(y.resolve(),y=null),m&&(a.timeout.cancel(m),m=null)}let h=!1;e.on("dispose",()=>{h=!0,p++,b(),w&&w.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"click",M=>{e.symbolsOpen&&!M.target.closest?.(".symbols")&&(e.symbolsOpen=!1),e.moreOpen&&!M.target.closest?.(".tb-more")&&(e.moreOpen=!1),e.tocOpen&&!M.target.closest?.(".tb-toc-m")&&(e.tocOpen=!1),e.gotoOpen&&M.target.isConnected&&!M.target.closest?.(".goto-pop, .btn-goto, .tb-more")&&(e.gotoOpen=!1),e.accountOpen&&!M.target.closest?.(".acct")&&(e.accountOpen=!1),e.helpOpen&&!M.target.closest?.(".vb-help")&&(e.helpOpen=!1)}),u(document,"keydown",M=>{M.key==="Escape"&&(e.symbolsOpen=!1,e.moreOpen=!1,e.tocOpen=!1,e.gotoOpen=!1,e.accountOpen=!1,e.helpOpen=!1),(M.ctrlKey||M.metaKey)&&!M.altKey&&M.key&&M.key.toLowerCase()==="f"&&e.type==="html"&&window.document.querySelector(".reading-layout")&&(M.preventDefault(),e.toggleMdFind(!0))}),u(document,"keydown",function(M){if((M.metaKey||M.ctrlKey)&&M.key==="k"){M.preventDefault();var F=document.querySelector(".tree-search-input");F&&(F.focus(),F.select())}});var w=null;e.onFileSearchChange=function(){w&&(w.resolve(),w=null);let M=e.fileSearchQuery;if(!M||M.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let F=r.defer();w=F,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(M)}`,{timeout:F.promise}).then(function(B){if(!(h||w!==F)){w=null,e.fileSearchLoading=!1;var K={};e.files.forEach(function(pe){K[(pe.path||"")+"/"+pe.name]=!0});for(var E=[],S={},O=0;O<B.data.length;O++){var H=B.data[O];if(H.path)for(var z=H.path.split("/"),Y="",te=0;te<z.length;te++){var J=Y;Y=Y?Y+"/"+z[te]:z[te];var Z=J+"/"+z[te];!K[Z]&&!S[Z]&&(S[Z]=!0,E.push({name:z[te],path:J}))}}E.sort(function(pe,De){return(pe.path||"").split("/").length-(De.path||"").split("/").length}),E.length>0&&e.files.push.apply(e.files,E);for(var G=[],ge=0;ge<B.data.length;ge++){var ae=B.data[ge],de=(ae.path||"")+"/"+ae.name;!K[de]&&ae.size!=null&&(G.push(ae),K[de]=!0)}G.length>0&&e.files.push.apply(e.files,G),e.fileSearchResults=B.data}},function(){!h&&w===F&&(w=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let A={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},N=["license","txt"],R=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],v=["wav","mp3","ogg","wma","flac","aac","m4a"],C=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(M,F){if(e.repoId!=i.repoId)return he();if((i.path||"")!=e.filePath){window.matchMedia?.("(max-width: 767px)").matches&&(e.sidebarCollapsed=!0),e.filePath=i.path||"",e.paths=e.filePath.split("/").filter(B=>B&&B.trim().length>0),et();for(let B=0;B<e.paths.length-1;B++){let K=B>0?e.paths.slice(0,B).join("/"):"";e.files.some(S=>S.path===K)||e.getFiles(K)}}});function I(){if(e.paths[0]!="")return;let M=["readme.md","readme.txt","readme.org","readme.1st","readme"],F={};for(let K of e.files)K.name.toLowerCase().indexOf("readme")>-1&&(F[K.name.toLowerCase()]=K.name);let B=null;for(let K of M)if(F[K]){B=K;break}if(!B&&Object.keys(F).length>0&&(B=Object.keys(F)[0]),B){let K=o.url();K[K.length-1]!="/"&&(K+="/"),o.url(K+encodePathForUrl(F[B]))}}e.fileCounts=null;let V=0,q=new Set,W=!1;e.getFiles=function(M,F=!1){let B=e.repoId,K=V;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(M)}&v=${e.options.lastUpdateDate}`).then(function(E){if(h||B!==e.repoId||K!==V)return[];let S=M||"";return e.files=e.files.filter(O=>O.path!==S),e.file
Line truncated
|
||||
`).forEach((F,B)=>{for(let[K,E]of Ae){let S=K.exec(F);if(S&&!/^(if|for|while|switch|return|catch)$/.test(S[1])){M.push({name:S[1],kind:E,line:B+1});break}}}),be={content:e.content,list:M.slice(0,300)},be.list},e.goToSymbol=function(M){e.symbolsOpen=!1;let F=e.codeEditor;F&&(F.scrollToLine(M-1,!0,!0,()=>{}),F.gotoLine(M,0,!0),window.history.replaceState(null,"",window.location.pathname+window.location.search+"#L"+M),window.dispatchEvent(new HashChangeEvent("hashchange")))},e.gotoLine=function(){e.codeEditor&&(e.moreOpen=!1,e.gotoError="",e.gotoValue=String(e.codeEditor.getCursorPosition().row+1),e.gotoOpen=!0,setTimeout(()=>{let M=window.document.getElementById("goto-input");M&&(M.focus(),M.select())},0))},e.closeGoto=function(){e.gotoOpen=!1,e.gotoError="",e.codeEditor?.focus()},e.submitGoto=function(){let M=e.codeEditor;if(!M)return;let F=/^\s*:?\s*(\d+)\s*(?:[:,]\s*(\d+))?\s*$/.exec(e.gotoValue||""),B=M.session.getLength();if(!F||+F[1]<1||+F[1]>B){e.gotoError="Enter a line from 1 to "+B;return}M.gotoLine(+F[1],F[2]?Math.max(0,+F[2]-1):0,!0),e.gotoOpen=!1,e.gotoError="",M.focus()},e.rootEntries=function(){return[...e.files||[]].sort((M,F)=>{let B=M.size==null,K=F.size==null;return B!==K?B?-1:1:M.name.localeCompare(F.name)})},e.formatSize=function(M){if(M==null)return"";if(M<1024)return M+" B";let F=M/1024,B=K=>K.toFixed(1).replace(/\.0$/,"");return F<1024?B(F)+" KB":F<1024*1024?B(F/1024)+" MB":B(F/1024/1024)+" GB"},e.fileSize=function(M){if(!M)return null;let F=M.replace(/^\/+/,""),B=(e.files||[]).find(K=>(K.path?K.path+"/":"")+K.name===F);return B&&B.size!=null?B.size:null},e.revealFolder=function(M){e.sidebarHidden=!1,e.sidebarCollapsed=!1,ot(()=>{let F=window.document.querySelector('.files a[data-path="/'+(window.CSS?.escape?window.CSS.escape(M):M)+'"]');F&&(F.closest("li")?.classList.contains("open")||F.click(),F.scrollIntoView({block:"nearest"}),F.focus())})};let le={content:null,n:0},fe=M=>{if(le.content===M)return le.n;let F=M;if(typeof F=="string")try{F=JSON.parse(F)}catch{F=null}let B=[],K=S=>{Array.isArray(S)?B.push(S.join("")):typeof S=="string"&&B.push(S)};(F&&(F.cells||(F.worksheets||[]).flatMap(S=>S.cells||[]))||[]).forEach(S=>{K(S?.source??S?.input),(S?.outputs||[]).forEach(O=>{K(O?.text),O?.data&&(K(O.data["text/plain"]),K(O.data["text/markdown"]),K(O.data["text/html"])),K(O?.traceback)})});let E=B.reduce((S,O)=>S+(O.match(uo)||[]).length,0);return le={content:M,n:E},E};e.redactionCount=function(){return e.type==="IPython"?fe(e.content):typeof e.content!="string"?0:((e.type==="html"?e.content.replace(/<[^>]*>/g," "):e.content).match(uo)||[]).length},e.renderRedacted=Oi;let ke=M=>M.toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/\s+/g,"-").slice(0,60)||"section";e.renderReadme=function(M){let F=e.renderRedacted(M);if(!F||typeof window.DOMParser!="function")return F;let B=new window.DOMParser().parseFromString("<div>"+F+"</div>","text/html"),K={};return B.querySelectorAll("h1, h2, h3").forEach(E=>{let S=E.id||ke(E.textContent);K[S]?S+="-"+ ++K[S]:K[S]=1,E.id=S}),B.body.firstChild.innerHTML};let Te={content:null,list:[]};e.readmeToc=function(){if(e.type!=="html"||typeof e.content!="string")return[];if(Te.content===e.content)return Te.list;let F=[...new window.DOMParser().parseFromString("<div>"+e.renderReadme(e.sanitize?e.sanitize(e.content):e.content)+"</div>","text/html").querySelectorAll("h2, h3")].map(B=>({id:B.id,text:B.textContent.trim(),level:B.tagName==="H3"?3:2})).slice(0,40);return Te={content:e.content,list:F},F},e.activeHeading=null,u(window,"scroll",()=>{let M=e.readmeToc?e.readmeToc():[];if(!M.length)return;let F=150,B="",K="";for(let E of M){let S=window.document.getElementById(E.id);if(!S)continue;let O=S.getBoundingClientRect().top;O<F?B=E.id:!K&&O<window.innerHeight-80&&(K=E.id)}K&&(B=K),e.activeHeading!==B&&(e.activeHeading=B)},{capture:!0,passive:!0}),e.scrollToHeading=function(M){if(!M){(window.document.querySelector(".explorer-content, .file-card-body, .main-pane")||window).scrollTo({top:0,behavior:"smooth"}),e.activeHeading="";return}let F=window.document.getElementById(M);F&&F.scrollIntoView({behavior:"smooth",block:"start"}),e.activeHeading=M},e.csvView="table";let Xe={content:null,rows:[]},rt=(M,F)=>{let B=[],K=[],E="",S=!1;for(let O=0;O<M.length;O++){let H=M[O];S?H==='"'?M[O+1]==='"'?(E+='"',O++):S=!1:E+=H:H==='"'&&E===""?S=!0:H===F?(K.push(E),E=""):H===`
|
||||
`||H==="\r"?(H==="\r"&&M[O+1]===`
|
||||
`&&O++,K.push(E),B.push(K),K=[],E=""):E+=H}return(E!==""||K.length)&&(K.push(E),B.push(K)),B.filter(O=>O.length>1||O[0]!=="")};e.isCsv=()=>typeof e.content=="string"&&e.type!=="html"&&/\.(csv|tsv)$/i.test(e.filePath||"")&&e.content.length<=1024*1024,e.csvRows=()=>{if(Xe.content!==e.content){let M=/\.tsv$/i.test(e.filePath||"")?" ":",";Xe={content:e.content,rows:rt(e.content,M)}}return Xe.rows},e.csvNumCol=M=>{let F=e.csvRows().slice(1).map(B=>B[M]).filter(B=>B!=null&&String(B).trim()!=="");return F.length>0&&F.every(B=>e.csvNumeric(B))},e.csvNumeric=M=>/^\s*[-+]?(\d[\d,]*)?(\.\d+)?(e[-+]?\d+)?%?\s*$/i.test(M)&&/\d/.test(M),e.redactParts=function(M){let F=[],B=0,K,E=/\bXXXX-(\d+)\b/g;for(M=String(M||"");K=E.exec(M);)K.index>B&&F.push({t:M.slice(B,K.index)}),F.push({n:K[1]}),B=K.index+K[0].length;return B<M.length&&F.push({t:M.slice(B)}),F},e.pathCopied=!1,e.copyPath=async function(){window.navigator.clipboard?.writeText&&(await window.navigator.clipboard.writeText(e.filePath||""),e.pathCopied=!0,setTimeout(()=>{e.pathCopied=!1},1600))},e.contentCopied=!1,e.copyContent=async function(){if(typeof e.content!="string"||!window.navigator.clipboard?.writeText)return;let M=e.content;if(e.type==="html"&&e.url)try{M=await(await window.fetch(e.url,{credentials:"same-origin"})).text()}catch{return}await window.navigator.clipboard.writeText(M),e.contentCopied=!0,setTimeout(()=>{e.contentCopied=!1},1600)};function at(M){return A[M]?A[M]:M}function Qe(M){return M=="pdf"?"pdf":M=="html"||M=="htm"?"html-doc":M=="md"?"md":M=="org"?"org":M=="ipynb"?"IPython":N.indexOf(M)>-1?"text":R.indexOf(M)>-1?"image":C.indexOf(M)>-1?"media":v.indexOf(M)>-1?"audio":"code"}function wt(M,F){let B=p;if(!M){e.type="error",e.content="no_file_selected";return}let K=e.type;(K==="code"||K==="text")&&qs().catch(()=>{}),e.type="loading",e.content="loading",e.fileLoadingSlow=!1,e.fileReceivedBytes=0;let E=r.defer();y=E,m=a.timeout(()=>{!h&&B===p&&e.type==="loading"&&(e.fileLoadingSlow=!0)},1500);let S=F&&F.sha||"0";t.get(`/api/repo/${e.repoId}/file/${encodePathForUrl(M)}?v=`+S,{timeout:E.promise,onDownloadProgress:O=>{!h&&B===p&&(e.fileReceivedBytes=O)},transformResponse:O=>O}).then(O=>{if(!(h||B!==p)){if(b(),e.type=K,e.content=O.data,e.content==""&&(e.content=null),e.type=="md"&&(e.content=renderMD(O.data,o.url()+"/../"),e.type="html"),e.type=="org"){let z=contentAbs2Relative(O.data);var H=new Org.Parser().parse(z).convert(Org.ConverterHTML,{headerOffset:1,exportFromLineNumber:!1,suppressSubScriptHandling:!0,suppressAutoLink:!1});e.content=DOMPurify.sanitize(H.toString()),e.type="html"}e.type=="code"&&O.headers("content-type")=="application/octet-stream"&&(e.type="binary",e.content="binary"),a.timeout(()=>{Prism.highlightAll()},50)}},O=>{h||B!==p||(b(),e.type="error",e.content=Ca(O))})}function et(){p++,b(),e.content="",e.file=Q();let M="0";e.file&&e.file.sha&&(M=e.file.sha),e.url=`/api/repo/${e.repoId}/file/${encodePathForUrl(e.filePath)}?v=${M}`;let F=e.filePath.substring(0,e.filePath.lastIndexOf("/")+1);e.fileBaseUrl=`/api/repo/${e.repoId}/file/${F?encodePathForUrl(F):""}`,e.showSource=!1,e.imageSize="",e.allowScripts=!1;let B=e.filePath.toLowerCase(),K=B.lastIndexOf(".");K>-1&&(B=B.substring(K+1));let E={javascript:"JavaScript",typescript:"TypeScript",tsx:"TSX",jsx:"JSX",python:"Python",java:"Java",c_cpp:"C/C++",csharp:"C#",golang:"Go",rust:"Rust",ruby:"Ruby",php:"PHP",html:"HTML",css:"CSS",scss:"SCSS",json:"JSON",yaml:"YAML",markdown:"Markdown",sh:"Shell",dockerfile:"Dockerfile",xml:"XML",sql:"SQL",kotlin:"Kotlin",swift:"Swift",scala:"Scala",r:"R",julia:"Julia",latex:"LaTeX",toml:"TOML",csv:"CSV",tsv:"TSV",text:""},S=at(B);if(e.languageName=E[S]??(S?S.charAt(0).toUpperCase()+S.slice(1):""),e.aceOption={readOnly:!0,useWrapMode:!0,showGutter:!0,theme:"chrome",useSoftTab:!0,tabSize:2,fontSize:window.matchMedia?.("(max-width: 767px)").matches?12:13,keyBinding:"vscode",fullLineSelection:!0,highlightActiveLine:!1,highlightGutterLine:!1,cursor:"hide",showInvisibles:!1,showIndentGuides:!1,showPrintMargin:!1,highlightSelectedWord:!1,enableBehaviours:!0,fadeFoldWidgets:!1,mode:at(B),onLoad:function(O){e.codeEditor=bo(O);{let de=O.session,pe=de.getTokens.bind(de),De=/\bXXXX-\d+\b/g;if(de.getTokens=function(Re){let _e=pe(Re);if(!_e.some(Se=>Se.value.indexOf("XXXX-")!==-1))return _e;let me=[];for(let Se of _e){if(Se.value.indexOf("XXXX-")===-1){me.push(Se);continue}let ze=0;De.lastIndex=0;let Fe;for(;Fe=De.exec(Se.value);)Fe.index>ze&&me.push({type:Se.type,value:Se.value.slice(ze,Fe.index)}),me.push({type:"redacted.n"+Fe[0].slice(5),value:Fe[0]}),ze=Fe.index+Fe[0].length;ze<Se.value.length&&me.push({type:Se.type,value:Se.value.slice(ze)})}return me},!window.document.getElementById("redact-index-rules")){let Re=window.document.createElement("style");Re.id="redact-index-rules",Re.textContent=Array.from({length:99},(_e,me)=>`.ace_redacted.ace_n${me+1}::after{content:"${me+1}"}`).join(""),window.document.head.appendChild(Re)}O.renderer.updateFull(!0)}O.session
Line truncated
|
||||
`).length+1)),placeholder:`Jane Smith
|
||||
MIT CSAIL
|
||||
jane.smith@example.org`},null,10,_A),[[s,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"anonymize",options:{debounce:250}}]]),t[93]||(t[93]=n("small",{class:"form-text text-muted"},"One term per line.",-1)),n("details",OA,[t[88]||(t[88]=n("summary",null,"Regex & replacement syntax",-1)),t[89]||(t[89]=y()),n("small",IA,[t[82]||(t[82]=y("Regex is supported. Replaced by ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK)+"-[N]",1),t[83]||(t[83]=y(", or use ",-1)),t[84]||(t[84]=n("code",null,"term=>replacement",-1)),t[85]||(t[85]=y(" to pick your own (e.g. ",-1)),t[86]||(t[86]=n("code",null,"Anonymous=>ABC",-1)),t[87]||(t[87]=y(").",-1))])]),(l(!0),d(B,null,ie((e.termHints||[]).slice(0,3),a=>(l(),d("div",{class:"term-hint",key:a.line},[n("span",null,[t[90]||(t[90]=y("In ",-1)),n("code",null,c(a.term),1),t[91]||(t[91]=y(", ",-1)),n("code",null,c(a.symbols.join(" ")),1),y(" "+c(a.symbols.length>1?"are regex symbols":"is a regex symbol")+", so it may not match the text as written.",1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm",onClick:u=>e.matchTermAsWritten(a.line)},"Match as written",8,MA)]))),128)),(e.termHints||[]).length>3?(l(),d("div",VA,c(e.termHints.length-3)+" more "+c(e.termHints.length-3===1?"term uses":"terms use")+" regex symbols.",1)):f("v-if",!0),A(n("div",qA,"Terms are in an invalid format.",512),[[J,e.anonymize?.terms?.errors?.format]])]),A(n("div",$A,[n("div",LA,[t[94]||(t[94]=n("span",{class:"paper-field-label",id:"preserve-files-label"},"Unredacted files",-1)),n("span",UA,c(e.preserveSummary()),1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm","aria-describedby":"preserve-files-label",disabled:!e.source?.commit,onClick:t[19]||(t[19]=a=>e.openPreserveFiles())},c((e.options?.preserveFiles||[]).length?"Edit":"Choose files"),9,FA)],512),[[J,e.detectedType==="repo"]]),e.preserve?.open?(l(),d("dialog",{key:0,id:"preserve-files-dialog",class:"preserve-dialog","aria-labelledby":"preserve-files-title",onCancel:t[24]||(t[24]=ce(a=>e.closePreserveFiles(),["prevent"]))},[n("form",{onSubmit:t[23]||(t[23]=ce(a=>e.savePreserveFiles(),["prevent"]))},[t[98]||(t[98]=n("h2",{id:"preserve-files-title"},"Leave files unredacted",-1)),t[99]||(t[99]=n("p",{class:"preserve-intro"},"Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.",-1)),A(n("input",{type:"search",id:"preserve-files-search",class:"form-control",placeholder:"Search files","aria-label":"Search files",autocomplete:"off","onUpdate:modelValue":t[20]||(t[20]=a=>e.preserve.search=a)},null,512),[[ct,e.preserve.search]]),n("div",BA,[e.preserve.loading?(l(),d("p",zA,"Loading files from GitHub\u2026")):e.preserve.error?(l(),d("p",HA,c(e.preserve.error),1)):(l(),d(B,{key:2},[(l(!0),d(B,null,ie(e.preserveRows(),(a,u)=>(l(),d("div",{class:"preserve-item",key:a.kind+":"+a.path,style:$e({paddingLeft:a.depth*18+6+"px"})},[a.kind==="dir"?(l(),d("button",{key:0,type:"button",class:"preserve-caret","aria-expanded":a.open?"true":"false","aria-label":(a.open?"Collapse ":"Expand ")+a.label,onClick:p=>e.togglePreserveDir(a.path)},c(a.open?"\u25BE":"\u25B8"),9,jA)):(l(),d("span",GA)),n("input",{type:"checkbox",id:"pf-"+u,"aria-label":a.kind==="dir"?"Leave folder "+a.path+" unredacted":null,checked:a.state==="all",".indeterminate":a.state==="some",disabled:a.locked,onChange:p=>a.kind==="dir"?e.togglePreserveFolder(a.path):e.togglePreserveFile(a.path)},null,40,WA),n("label",{for:a.kind==="file"?"pf-"+u:null,class:"preserve-name",onClick:p=>a.kind==="dir"&&e.togglePreserveDir(a.path)},c(a.label),9,KA),a.note?(l(),d("span",YA,c(a.note),1)):f("v-if",!0)],4))),128)),!e.preserve.loading&&!e.preserveRows().length?(l(),d("p",XA,c(e.preserve.search?"No file matches your search.":"No text files in this commit."),1)):f("v-if",!0)],64))]),e.preserve.truncated?(l(),d("p",JA,"This repository has too many files to list them all. Use a pattern for files you cannot find.")):f("v-if",!0),n("details",{class:"preserve-patterns",open:e.preserve.patternsOpen},[t[95]||(t[95]=n("summary",null,"Patterns",-1)),A(n("textarea",{class:"form-control",rows:"3","aria-label":"Patterns",placeholder:`*.bib
|
||||
paper/appendix/**`,"onUpdate:modelValue":t[21]||(t[21]=a=>e.preserve.patterns=a)},null,512),[[ct,e.preserve.patterns]]),t[96]||(t[96]=n("small",{class:"form-text text-muted"},[y("One per line, as in .gitignore: "),n("code",null,"*.bib"),y(" matches in any folder; "),n("code",null,"/refs.bib"),y(" and "),n("code",null,"paper/refs.bib"),y(" match from the repository root; a folder name such as "),n("code",null,"drafts/"),y(" covers everything inside it; "),n("code",null,"!private.bib"),y(" excludes again.")],-1))],8,QA),n("div",ZA,[n("span",xA,c(e.preserve.loading||e.preserve.error?"":e.preserveCount()+(e.preserveCount()===1?" file":" files")+" will be shown as written"),1),n("span",eT,[n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[22]||(t[22]=a=>e.closePreserveFiles())},"Cancel"),t[97]||(t[97]=n("button",{type:"submit",class:"btn btn-ink"},"Save",-1))])])],32)],32)):f("v-if",!0)],512),[[J,e.detectedType]]),A(n("section",tT,[t[120]||(t[120]=n("h2",{class:"paper-section-eyebrow"},"Display",-1)),A(n("div",null,[n("div",nT,[A(n("input",oT,null,512),[[s,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"anonymize",options:{}}]]),t[101]||(t[101]=n("label",{class:"form-check-label",for:"link"},"Keep links",-1))]),n("div",sT,[A(n("input",iT,null,512),[[s,{state:e.viewState,set:a=>{e.options.image=a},value:e.options?.image,form:"anonymize",options:{}}]]),t[102]||(t[102]=n("label",{class:"form-check-label",for:"image"},"Display images",-1))]),n("div",rT,[A(n("input",aT,null,512),[[s,{state:e.viewState,set:a=>{e.options.pdf=a},value:e.options?.pdf,form:"anonymize",options:{}}]]),t[103]||(t[103]=n("label",{class:"form-check-label",for:"pdf"},"Display PDFs",-1))]),n("div",lT,[A(n("input",dT,null,512),[[s,{state:e.viewState,set:a=>{e.options.notebook=a},value:e.options?.notebook,form:"anonymize",options:{}}]]),t[104]||(t[104]=n("label",{class:"form-check-label",for:"notebook"},"Display Notebooks",-1))]),n("div",uT,[A(n("input",{class:"form-check-input",type:"checkbox",id:"page",name:"page",disabled:!e.details?.hasPage||e.details?.pageSource&&e.details?.pageSource?.branch!==e.source?.branch},null,8,cT),[[s,{state:e.viewState,set:a=>{e.options.page=a},value:e.options?.page,form:"anonymize",options:{}}]]),t[105]||(t[105]=n("label",{class:"form-check-label",for:"page"},"GitHub Pages",-1)),e.details?.hasPage?f("v-if",!0):(l(),d("small",pT,c(e.fmt?.translate("WARNINGS.page_not_enabled_on_repo")),1)),e.details?.hasPage&&e.details?.pageSource?.branch&&e.source?.branch&&e.details.pageSource.branch!==e.source.branch?(l(),d("small",fT,c(e.fmt?.translate("WARNINGS.page_branch_mismatch",{pageBranch:e.details?.pageSource?.branch,selectedBranch:e.source?.branch})),1)):f("v-if",!0)])],512),[[J,e.detectedType==="repo"]]),e.detectedType==="repo"&&(e.options.image||e.options.pdf)?(l(),d("p",hT,[...t[106]||(t[106]=[y("Images and PDFs are served unchanged. Review their contents and metadata before sharing. ",-1),n("a",{href:"/faq#formats"},"Supported formats",-1)])])):f("v-if",!0),A(n("div",null,[n("div",mT,[A(n("input",gT,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[107]||(t[107]=n("label",{class:"form-check-label",for:"title-gist"},"Gist description",-1))]),n("div",vT,[A(n("input",yT,null,512),[[s,{state:e.viewState,set:a=>{e.options.content=a},value:e.options?.content,form:"anonymize",options:{}}]]),t[108]||(t[108]=n("label",{class:"form-check-label",for:"content-gist"},"File contents",-1))]),n("div",bT,[A(n("input",wT,null,512),[[s,{state:e.viewState,set:a=>{e.options.comments=a},value:e.options?.comments,form:"anonymize",options:{}}]]),t[109]||(t[109]=n("label",{class:"form-check-label",for:"comments-gist"},"Comments",-1))]),n("div",kT,[A(n("input",CT,null,512),[[s,{state:e.viewState,set:a=>{e.options.username=a},value:e.options?.username,form:"anonymize",options:{}}]]),t[110]||(t[110]=n("label",{class:"form-check-label",for:"username-gist"},"Usernames",-1))]),n("div",ET,[A(n("input",ST,null,512),[[s,{state:e.viewState,set:a=>{e.options.date=a},value:e.options?.date,form:"anonymize",options:{}}]]),t[111]||(t[111]=n("label",{class:"form-check-label",for:"date-gist"},"Dates",-1))]),n("div",NT,[A(n("input",RT,null,512),[[s,{state:e.viewState,set:a=>{e.options.origin=a},value:e.options?.origin,form:"anonymize",options:{}}]]),t[112]||(t[112]=n("label",{class:"form-check-label",for:"origin-gist"},"Source gist ID",-1))])],512),[[J,e.detectedType==="gist"]]),A(n("div",null,[n("div",DT,[A(n("input",AT,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[113]||(t[113]=n("label",{class:"form-check-label",for:"title"},"PR title",-1))]),n("div",TT,[A(n("input",PT,null,512),[[s,{state:e.viewState,set:a=>{e.options.body=a},value:e.options?.body,form:"anonymize",options:{}}]]),t[114]||(t[114]=n("label",{class:"form-check-label",for:"body"},"PR body",-1))]),n("div",_T,[A(n("input"
Line truncated
|
||||
anonymous_github`)])])])])]),f(" Limitations "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-warning"})]),y("Limitations ")]),n("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),n("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[n("div",{class:"panel-body p-3"},[n("ul",null,[n("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),n("li",null," Files larger than 8 MB are not supported. "),n("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),n("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy & Security "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-shield"})]),y("Privacy & Security ")]),n("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),n("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[n("div",{class:"panel-body p-3"},[n("p",null," Repository data is used to provide the anonymization service. If you purchase a plan, Stripe processes payment and billing information. When a repository is removed or expires, only its configuration is retained. This makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),n("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[n("div",{class:"panel-body p-3"},[n("p",null,[y("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),n("a",{href:"/connections"},"GitHub connections"),y(" page.")]),n("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),n("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),n("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[n("div",{class:"panel-body p-3"},[n("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-Hosting "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#project-icon-storage"})]),y("Self-Hosting ")]),n("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),n("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[n("div",{class:"panel-body p-3"},[n("p",null,[y(" Yes.
Line truncated
|
||||
jane.smith@example.org`},null,10,QT),[[s,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"anonymize",options:{debounce:250}}]]),t[115]||(t[115]=n("small",{class:"form-text text-muted"},"One term per line.",-1)),n("details",ZT,[t[110]||(t[110]=n("summary",null,"Regex & replacement syntax",-1)),t[111]||(t[111]=g()),n("small",xT,[t[104]||(t[104]=g("Regex is supported. Replaced by ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK)+"-[N]",1),t[105]||(t[105]=g(", or use ",-1)),t[106]||(t[106]=n("code",null,"term=>replacement",-1)),t[107]||(t[107]=g(" to pick your own (e.g. ",-1)),t[108]||(t[108]=n("code",null,"Anonymous=>ABC",-1)),t[109]||(t[109]=g(").",-1))])]),(l(!0),d(L,null,ie((e.termHints||[]).slice(0,3),a=>(l(),d("div",{class:"term-hint",key:a.line},[n("span",null,[t[112]||(t[112]=g("In ",-1)),n("code",null,c(a.term),1),t[113]||(t[113]=g(", ",-1)),n("code",null,c(a.symbols.join(" ")),1),g(" "+c(a.symbols.length>1?"are regex symbols":"is a regex symbol")+", so it may not match the text as written.",1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm",onClick:u=>e.matchTermAsWritten(a.line)},"Match as written",8,e3)]))),128)),(e.termHints||[]).length>3?(l(),d("div",t3,c(e.termHints.length-3)+" more "+c(e.termHints.length-3===1?"term uses":"terms use")+" regex symbols.",1)):f("v-if",!0),P(n("div",n3,"Terms are in an invalid format.",512),[[ee,e.anonymize?.terms?.errors?.format]])]),P(n("div",o3,[n("div",s3,[t[116]||(t[116]=n("span",{class:"paper-field-label",id:"preserve-files-label"},"Unredacted files",-1)),n("span",i3,c(e.preserveSummary()),1)]),n("button",{type:"button",class:"btn btn-outline-ink btn-sm","aria-describedby":"preserve-files-label",disabled:!e.source?.commit,onClick:t[21]||(t[21]=a=>e.openPreserveFiles())},c((e.options?.preserveFiles||[]).length?"Edit":"Choose files"),9,r3)],512),[[ee,e.detectedType==="repo"]]),e.preserve?.open?(l(),d("dialog",{key:0,id:"preserve-files-dialog",class:"preserve-dialog","aria-labelledby":"preserve-files-title",onCancel:t[26]||(t[26]=ce(a=>e.closePreserveFiles(),["prevent"]))},[n("form",{onSubmit:t[25]||(t[25]=ce(a=>e.savePreserveFiles(),["prevent"]))},[t[120]||(t[120]=n("h2",{id:"preserve-files-title"},"Leave files unredacted",-1)),t[121]||(t[121]=n("p",{class:"preserve-intro"},"Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.",-1)),P(n("input",{type:"search",id:"preserve-files-search",class:"form-control",placeholder:"Search files","aria-label":"Search files",autocomplete:"off","onUpdate:modelValue":t[22]||(t[22]=a=>e.preserve.search=a)},null,512),[[dt,e.preserve.search]]),n("div",a3,[e.preserve.loading?(l(),d("p",l3,"Loading files from GitHub\u2026")):e.preserve.error?(l(),d("p",d3,c(e.preserve.error),1)):(l(),d(L,{key:2},[(l(!0),d(L,null,ie(e.preserveRows(),(a,u)=>(l(),d("div",{class:"preserve-item",key:a.kind+":"+a.path,style:$e({paddingLeft:a.depth*18+6+"px"})},[a.kind==="dir"?(l(),d("button",{key:0,type:"button",class:"preserve-caret","aria-expanded":a.open?"true":"false","aria-label":(a.open?"Collapse ":"Expand ")+a.label,onClick:p=>e.togglePreserveDir(a.path)},c(a.open?"\u25BE":"\u25B8"),9,u3)):(l(),d("span",c3)),n("input",{type:"checkbox",id:"pf-"+u,"aria-label":a.kind==="dir"?"Leave folder "+a.path+" unredacted":null,checked:a.state==="all",".indeterminate":a.state==="some",disabled:a.locked,onChange:p=>a.kind==="dir"?e.togglePreserveFolder(a.path):e.togglePreserveFile(a.path)},null,40,p3),n("label",{for:a.kind==="file"?"pf-"+u:null,class:"preserve-name",onClick:p=>a.kind==="dir"&&e.togglePreserveDir(a.path)},c(a.label),9,f3),a.note?(l(),d("span",h3,c(a.note),1)):f("v-if",!0)],4))),128)),!e.preserve.loading&&!e.preserveRows().length?(l(),d("p",m3,c(e.preserve.search?"No file matches your search.":"No text files in this commit."),1)):f("v-if",!0)],64))]),e.preserve.truncated?(l(),d("p",g3,"This repository has too many files to list them all. Use a pattern for files you cannot find.")):f("v-if",!0),n("details",{class:"preserve-patterns",open:e.preserve.patternsOpen},[t[117]||(t[117]=n("summary",null,"Patterns",-1)),P(n("textarea",{class:"form-control",rows:"3","aria-label":"Patterns",placeholder:`*.bib
|
||||
paper/appendix/**`,"onUpdate:modelValue":t[23]||(t[23]=a=>e.preserve.patterns=a)},null,512),[[dt,e.preserve.patterns]]),t[118]||(t[118]=n("small",{class:"form-text text-muted"},[g("One per line, as in .gitignore: "),n("code",null,"*.bib"),g(" matches in any folder; "),n("code",null,"/refs.bib"),g(" and "),n("code",null,"paper/refs.bib"),g(" match from the repository root; a folder name such as "),n("code",null,"drafts/"),g(" covers everything inside it; "),n("code",null,"!private.bib"),g(" excludes again.")],-1))],8,v3),n("div",y3,[n("span",b3,c(e.preserve.loading||e.preserve.error?"":e.preserveCount()+(e.preserveCount()===1?" file":" files")+" will be shown as written"),1),n("span",w3,[n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[24]||(t[24]=a=>e.closePreserveFiles())},"Cancel"),t[119]||(t[119]=n("button",{type:"submit",class:"btn btn-ink"},"Save",-1))])])],32)],32)):f("v-if",!0)],512),[[ee,e.detectedType]]),P(n("section",k3,[t[142]||(t[142]=n("h2",{class:"paper-section-eyebrow"},"Display",-1)),P(n("div",null,[n("div",C3,[P(n("input",E3,null,512),[[s,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"anonymize",options:{}}]]),t[123]||(t[123]=n("label",{class:"form-check-label",for:"link"},"Keep links",-1))]),n("div",S3,[P(n("input",N3,null,512),[[s,{state:e.viewState,set:a=>{e.options.image=a},value:e.options?.image,form:"anonymize",options:{}}]]),t[124]||(t[124]=n("label",{class:"form-check-label",for:"image"},"Display images",-1))]),n("div",R3,[P(n("input",D3,null,512),[[s,{state:e.viewState,set:a=>{e.options.pdf=a},value:e.options?.pdf,form:"anonymize",options:{}}]]),t[125]||(t[125]=n("label",{class:"form-check-label",for:"pdf"},"Display PDFs",-1))]),n("div",A3,[P(n("input",T3,null,512),[[s,{state:e.viewState,set:a=>{e.options.notebook=a},value:e.options?.notebook,form:"anonymize",options:{}}]]),t[126]||(t[126]=n("label",{class:"form-check-label",for:"notebook"},"Display notebooks",-1))]),n("div",P3,[P(n("input",{class:"form-check-input",type:"checkbox",id:"page",name:"page",disabled:!e.details?.hasPage||e.details?.pageSource&&e.details?.pageSource?.branch!==e.source?.branch},null,8,O3),[[s,{state:e.viewState,set:a=>{e.options.page=a},value:e.options?.page,form:"anonymize",options:{}}]]),t[127]||(t[127]=n("label",{class:"form-check-label",for:"page"},"GitHub Pages",-1)),e.details?.hasPage?f("v-if",!0):(l(),d("small",I3,c(e.fmt?.translate("WARNINGS.page_not_enabled_on_repo")),1)),e.details?.hasPage&&e.details?.pageSource?.branch&&e.source?.branch&&e.details.pageSource.branch!==e.source.branch?(l(),d("small",_3,c(e.fmt?.translate("WARNINGS.page_branch_mismatch",{pageBranch:e.details?.pageSource?.branch,selectedBranch:e.source?.branch})),1)):f("v-if",!0)])],512),[[ee,e.detectedType==="repo"]]),e.detectedType==="repo"&&(e.options.image||e.options.pdf)?(l(),d("p",M3,[...t[128]||(t[128]=[g("Images and PDFs are served unchanged. Review their contents and metadata before sharing. ",-1),n("a",{href:"/faq#formats"},"Supported formats",-1)])])):f("v-if",!0),P(n("div",null,[n("div",V3,[P(n("input",q3,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[129]||(t[129]=n("label",{class:"form-check-label",for:"title-gist"},"Gist description",-1))]),n("div",$3,[P(n("input",L3,null,512),[[s,{state:e.viewState,set:a=>{e.options.content=a},value:e.options?.content,form:"anonymize",options:{}}]]),t[130]||(t[130]=n("label",{class:"form-check-label",for:"content-gist"},"File contents",-1))]),n("div",U3,[P(n("input",F3,null,512),[[s,{state:e.viewState,set:a=>{e.options.comments=a},value:e.options?.comments,form:"anonymize",options:{}}]]),t[131]||(t[131]=n("label",{class:"form-check-label",for:"comments-gist"},"Comments",-1))]),n("div",B3,[P(n("input",z3,null,512),[[s,{state:e.viewState,set:a=>{e.options.username=a},value:e.options?.username,form:"anonymize",options:{}}]]),t[132]||(t[132]=n("label",{class:"form-check-label",for:"username-gist"},"Usernames",-1))]),n("div",H3,[P(n("input",j3,null,512),[[s,{state:e.viewState,set:a=>{e.options.date=a},value:e.options?.date,form:"anonymize",options:{}}]]),t[133]||(t[133]=n("label",{class:"form-check-label",for:"date-gist"},"Dates",-1))]),n("div",G3,[P(n("input",W3,null,512),[[s,{state:e.viewState,set:a=>{e.options.origin=a},value:e.options?.origin,form:"anonymize",options:{}}]]),t[134]||(t[134]=n("label",{class:"form-check-label",for:"origin-gist"},"Source gist ID",-1))])],512),[[ee,e.detectedType==="gist"]]),P(n("div",null,[n("div",K3,[P(n("input",Y3,null,512),[[s,{state:e.viewState,set:a=>{e.options.title=a},value:e.options?.title,form:"anonymize",options:{}}]]),t[135]||(t[135]=n("label",{class:"form-check-label",for:"title"},"PR title",-1))]),n("div",X3,[P(n("input",J3,null,512),[[s,{state:e.viewState,set:a=>{e.options.body=a},value:e.options?.body,form:"anonymize",options:{}}]]),t[136]||(t[136]=n("label",{class:"form-check-label",for:"body"},"PR body",-1))]),n("div",Q3,[P(n(
Line truncated
|
||||
anonymous_github`)])])])])]),f(" Limitations "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-warning"})]),g("Limitations ")]),n("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),n("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[n("div",{class:"panel-body p-3"},[n("ul",null,[n("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),n("li",null," Files larger than 8 MB are not supported. "),n("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),n("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy and security "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#ui-icon-shield"})]),g("Privacy and security ")]),n("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),n("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[n("div",{class:"panel-body p-3"},[n("p",null," Repository data is used to provide the anonymization service. If you purchase a plan, Stripe processes payment and billing information. When a repository is removed or expires, only its configuration is retained. This makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),n("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[n("div",{class:"panel-body p-3"},[n("p",null,[g("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),n("a",{href:"/connections"},"GitHub connections"),g(" page.")]),n("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),n("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),n("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[n("div",{class:"panel-body p-3"},[n("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-hosting "),n("h4",{class:"faq-cat"},[n("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[n("use",{href:"#project-icon-storage"})]),g("Self"),n("span",{class:"serif-hy"},"-"),g("hosting ")]),n("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[n("div",{class:"panel panel-default mb-3"},[n("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[n("h3",{class:"panel-title"},[n("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),n("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[n("div",{cla
Line truncated
|
||||
anonymized repository often think it is about the paper, so the
|
||||
label names the service and the tooltip says who to contact. `),t[26]||(t[26]=ke('<li class="nav-item d-none d-lg-block"><a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug </a></li><li class="nav-item nav-support-item d-none d-lg-block"><a class="nav-link nav-support-inline" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs"><svg class="ui-icon nav-heart" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>Support </a></li>',2)),e.isDarkMode?f("v-if",!0):(l(),d("li",jV,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to dark mode","aria-label":"Switch to dark mode",onClick:t[1]||(t[1]=o=>{e.darkMode(!0)})},[...t[19]||(t[19]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",GV,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to light mode","aria-label":"Switch to light mode",onClick:t[2]||(t[2]=o=>{e.darkMode(!1)})},[...t[20]||(t[20]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-sun"})],-1)])])])):f("v-if",!0),!e.user&&!e.isReviewerRoute?(l(),d("li",WV,[...t[21]||(t[21]=[n("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[n("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])):f("v-if",!0),e.user?.username&&e.headerCanUpgrade&&!e.headerBillingPending?(l(),d("li",KV,[n("a",{class:"nav-link nav-upgrade",href:"/profile?upgrade=1",onClick:t[3]||(t[3]=ce(o=>e.openHeaderUpgrade(),["prevent"]))},"Upgrade")])):f("v-if",!0),e.user?(l(),d("li",YV,[n("a",XV,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,JV)):f("v-if",!0),n("span",QV,c(e.user?.username),1)]),n("div",ZV,[n("h6",xV,"Signed in as @"+c(e.user?.username),1),t[23]||(t[23]=ke('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item um-extra" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a>',6)),e.headerShowSupport?(l(),d("a",eq,[...t[22]||(t[22]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-heart"})],-1),n("span",null,"Support the project",-1)])])):f("v-if",!0),t[24]||(t[24]=n("div",{class:"dropdown-divider"},null,-1)),t[25]||(t[25]=n("a",{class:"dropdown-item",href:"/api/user/logout",target:"_self"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-logout"})]),n("span",null,"Sign out")],-1))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,tq)):f("v-if",!0),e.emailPromptOpen?(l(),d("dialog",{key:1,id:"email-prompt",class:"email-prompt","aria-labelledby":"email-prompt-title","aria-describedby":"email-prompt-reason",onCancel:t[8]||(t[8]=ce(o=>e.respondToEmailPrompt("later"),["prevent"]))},[n("form",{onSubmit:t[7]||(t[7]=ce(o=>e.respondToEmailPrompt("save"),["prevent"]))},[t[29]||(t[29]=n("h2",{id:"email-prompt-title"},"Stay informed about access problems",-1)),t[30]||(t[30]=n("p",{id:"email-prompt-reason"},"Get email alerts when your GitHub token or repository access fails.",-1)),t[31]||(t[31]=n("label",{for:"email-prompt-address"},"Email address",-1)),A(n("input",{id:"email-prompt-address",class:"form-control",type:"email",autocomplete:"email",maxlength:"254",required:"","onUpdate:modelValue":t[4]||(t[4]=o=>e.emailPromptAddress=o),disabled:e.emailPromptBusy},null,8,nq),[[ct,e.emailPromptAddress]]),e.emailPromptError?(l(),d("p",oq,c(e.emailPromptError),1)):f("v-if",!0),n("div",sq,[n("button",{type:"button",class:"btn btn-outline-ink",disabled:e.emailPromptBusy,onClick:t[5]||(t[5]=o=>e.respondToEma
Line truncated
|
||||
python experiments/run.py`),n("p",null,[y("Contact: "),n("span",{class:"redact",role:"img","aria-label":"Redacted term 3"},[n("i",null,"3")])])])])])])],-1))]),f(" Live stats "),n("section",hq,[t[9]||(t[9]=n("div",{class:"paper-stats-meta"},[n("div",{class:"paper-stats-meta-left"},[n("span",{class:"paper-stats-dot","aria-hidden":"true"}),y(" Live \xB7 last 60 days \xB7 updated daily")]),n("div",{class:"paper-stats-meta-right"},"Used by authors at ICSE \xB7 FSE \xB7 OOPSLA")],-1)),n("div",mq,[(l(!0),d(B,null,ie(e.cards,(o,i)=>(l(),d("div",{class:"paper-stat-card",key:o?.key},[n("div",{class:P(["paper-stat-value",{"is-pending":!e.stat}])},c(e.stat?e.fmt?.bigNum(o?.total):"\u2014"),3),n("div",gq,c(o?.label),1),e.history[o?.key].bars?.length>1?(l(),d("div",vq," +"+c(e.fmt?.number(e.history[o?.key].deltaToday))+" today ",1)):f("v-if",!0)]))),128))])]),f(" Before / after "),t[25]||(t[25]=n("section",{class:"paper-section-head paper-wrap"},[n("div",null,[n("div",{class:"paper-eyebrow"},"Try it"),n("h2",null,[y("The same repository, "),n("em",null,"minus you.")])]),n("p",null," Names, affiliations, emails, and any term you list are replaced across the README, source files, notebooks, and PDF links. Reviewers browse the real thing, not a zip. ")],-1)),n("div",yq,[n("div",bq,[t[10]||(t[10]=n("label",{for:"demo-terms"},[y("Terms to redact "),n("span",null,"comma-separated, try adding your own")],-1)),A(n("input",{id:"demo-terms",type:"text","onUpdate:modelValue":t[0]||(t[0]=o=>e.demoTerms=o),spellcheck:"false",autocomplete:"off"},null,512),[[ct,e.demoTerms]])]),n("div",wq,[n("div",kq,[t[12]||(t[12]=n("div",{class:"pane-header"},[n("span",{class:"pane-label"},[y("Your repository "),n("span",{class:"pane-edit-hint"},"\xB7 editable")]),n("span",{class:"pane-url"},"github.com/jane-smith/DeepLearnUtils")],-1)),n("div",Cq,[n("div",Eq,[(l(!0),d(B,null,ie(e.demoParts(),(o,i)=>(l(),d(B,{key:i},[o.redacted?(l(),d("mark",Sq,c(o.original),1)):(l(),d(B,{key:1},[y(c(o.text),1)],64))],64))),128)),t[11]||(t[11]=y("\u200B",-1))]),A(n("textarea",{class:"pane-body demo-input","onUpdate:modelValue":t[1]||(t[1]=o=>e.demoText=o),spellcheck:"false","aria-label":"Sample README to anonymize"},null,512),[[ct,e.demoText]])])]),n("div",Nq,[n("div",Rq,[t[13]||(t[13]=n("span",{class:"pane-label"},"What reviewers see",-1)),n("span",Dq,[n("span",Aq,c(e.demoCount())+" redacted",1)])]),n("div",Tq,[(l(!0),d(B,null,ie(e.demoParts(),(o,i)=>(l(),d(B,{key:i},[o.redacted?(l(),d("span",{key:0,class:"redact-chip",title:"Replaces \u201C"+o.original+"\u201D"},c(o.text),9,Pq)):(l(),d(B,{key:1},[y(c(o.text),1)],64))],64))),128))])])])]),f(" What you get: one screenshot, three tabs "),t[26]||(t[26]=n("section",{class:"paper-section-head paper-wrap",id:"about"},[n("div",null,[n("div",{class:"paper-eyebrow"},"What you get"),n("h2",null,"Anonymize, review, manage.")]),n("p",null," Terms are redacted in text files. Enabled images and PDFs are served unchanged, so review them for identifying content before sharing. Monitor your mirror, update it, or remove it when the review is over. ")],-1)),n("section",_q,[n("div",Oq,[(l(!0),d(B,null,ie(e.features,(o,i)=>(l(),d("div",{class:P(["paper-feature",{"is-on":e.feature===o?.key}]),key:o?.key},[n("button",{type:"button",class:"paper-feature-tab",role:"tab",onClick:s=>e.selectFeature(o.key),onKeydown:s=>e.featureKeydown(s,i),id:"feature-tab-"+o?.key,"aria-controls":"feature-panel-"+o?.key,"aria-selected":e.feature===o?.key,tabindex:e.feature===o?.key?0:-1},[n("span",Mq,c(o?.num)+" \xB7 "+c(o?.eyebrow),1),n("span",Vq,c(o?.title)+" "+c(o?.accent),1)],40,Iq),n("div",qq,[n("span",$q,c(o?.title)+" "+c(o?.accent),1),n("p",null,c(o?.text),1),n("a",{class:"paper-link-arrow",href:e.safeUrl(e.featureHref(o)),target:e.featureTarget(o)},c(o?.cta)+" \u2192",9,Lq)])],2))),128))]),n("div",Uq,[(l(!0),d(B,null,ie(e.features,(o,i)=>A((l(),d("div",{class:"paper-feature-panel",role:"tabpanel",key:o?.key,id:"feature-panel-"+o?.key,"aria-labelledby":"feature-tab-"+o?.key},[n("div",Bq,[n("div",zq,[t[14]||(t[14]=n("span",{class:"dots"},[n("i"),n("i"),n("i")],-1)),n("span",Hq,c(o?.url),1)]),n("div",{class:P(["feat-mock","is-"+o?.key]),role:"img","aria-label":o?.alt},[o?.key==="anonymize"?(l(),d("div",Gq,[...t[15]||(t[15]=[ke('<div class="fm-label">GitHub repository</div><div class="fm-input">github.com/jane-smith/DeepLearnUtils</div><div class="fm-label">Terms to redact</div><div class="fm-chips"><span>Jane Smith</span><span>MIT CSAIL</span><span>jane.smith@mit.edu</span><span class="fm-chip-add">+ Add term</span></div><div class="fm-row"><div><div class="fm-label">Reviewers can see</div><div class="fm-toggles"><span class="is-on">Images</span><span class="is-on">PDFs</span><span>Links</span><span class="is-on">Notebooks</span></div></div><div><div class="fm-label">Expires</div><div class="fm-input fm-input-sm">Mar 31, 2027</div></div></div><div class="fm-actions"><span class="fm-share">anonymous.4open.science/r/paper-7f2a</span><span class="fm-btn">
Line truncated
|
||||
label names the service and the tooltip says who to contact. `),t[26]||(t[26]=Ce('<li class="nav-item d-none d-lg-block"><a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug </a></li><li class="nav-item nav-support-item d-none d-lg-block"><a class="nav-link nav-support-inline" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs"><svg class="ui-icon nav-heart" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>Support </a></li>',2)),e.isDarkMode?f("v-if",!0):(l(),d("li",mU,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to dark mode","aria-label":"Switch to dark mode",onClick:t[1]||(t[1]=o=>{e.darkMode(!0)})},[...t[19]||(t[19]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",gU,[n("button",{type:"button",class:"nav-link nav-icon",title:"Switch to light mode","aria-label":"Switch to light mode",onClick:t[2]||(t[2]=o=>{e.darkMode(!1)})},[...t[20]||(t[20]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-sun"})],-1)])])])):f("v-if",!0),!e.user&&!e.isReviewerRoute?(l(),d("li",vU,[...t[21]||(t[21]=[n("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[n("i",{class:"fab fa-github mr-1"}),g(" Sign in ")],-1)])])):f("v-if",!0),e.user?.username&&e.headerCanUpgrade&&!e.headerBillingPending?(l(),d("li",yU,[n("a",{class:"nav-link nav-upgrade",href:"/profile?upgrade=1",onClick:t[3]||(t[3]=ce(o=>e.openHeaderUpgrade(),["prevent"]))},"Upgrade")])):f("v-if",!0),e.user?(l(),d("li",bU,[n("a",wU,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,kU)):f("v-if",!0),n("span",CU,c(e.user?.username),1)]),n("div",EU,[n("h6",SU,"Signed in as @"+c(e.user?.username),1),t[23]||(t[23]=Ce('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item um-extra" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a>',6)),e.headerShowSupport?(l(),d("a",NU,[...t[22]||(t[22]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-heart"})],-1),n("span",null,"Support the project",-1)])])):f("v-if",!0),t[24]||(t[24]=n("div",{class:"dropdown-divider"},null,-1)),t[25]||(t[25]=n("a",{class:"dropdown-item",href:"/api/user/logout",target:"_self"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-logout"})]),n("span",null,"Sign out")],-1))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,RU)):f("v-if",!0),e.emailPromptOpen?(l(),d("dialog",{key:1,id:"email-prompt",class:"email-prompt","aria-labelledby":"email-prompt-title","aria-describedby":"email-prompt-reason",onCancel:t[8]||(t[8]=ce(o=>e.respondToEmailPrompt("later"),["prevent"]))},[n("form",{onSubmit:t[7]||(t[7]=ce(o=>e.respondToEmailPrompt("save"),["prevent"]))},[t[29]||(t[29]=n("h2",{id:"email-prompt-title"},"Stay informed about access problems",-1)),t[30]||(t[30]=n("p",{id:"email-prompt-reason"},"Get email alerts when your GitHub token or repository access fails.",-1)),t[31]||(t[31]=n("label",{for:"email-prompt-address"},"Email address",-1)),P(n("input",{id:"email-prompt-address",class:"form-control",type:"email",autocomplete:"email",maxlength:"254",required:"","onUpdate:modelValue":t[4]||(t[4]=o=>e.emailPromptAddress=o),disabled:e.emailPromptBusy},null,8,DU),[[dt,e.emailPromptAddress]]),e.emailPromptError?(l(),d("p",AU,c(e.emailPromptError),1)):f("v-if",!0),n("div",TU,[n("button",{type:"button",class:"btn btn-outline-ink",disabled:e.emailPromptBusy,onClick:t[5]||(t[5]=o=>e.respondToEma
Line truncated
|
||||
python experiments/run.py`),n("p",null,[g("Contact: "),n("span",{class:"redact",role:"img","aria-label":"Redacted term 3"},[n("i",null,"3")])])])])])])],-1))]),f(" Live stats "),n("section",{class:T(["paper-stats paper-wrap",{"is-empty":e.statsEmpty()}]),id:"metrics","aria-label":"Usage over the last 60 days"},[n("div",UU,[e.statsEmpty()?f("v-if",!0):(l(),d("div",FU,[...t[11]||(t[11]=[n("span",{class:"paper-stats-dot","aria-hidden":"true"},null,-1),g(),n("span",null,[g("Live \xB7 last 60 days"),n("span",{class:"paper-stats-daily"}," \xB7 updated daily")],-1)])])),t[12]||(t[12]=n("div",{class:"paper-stats-meta-right"},"Used by authors at ICSE \xB7 FSE \xB7 OOPSLA",-1))]),e.statsEmpty()?f("v-if",!0):(l(),d("div",BU,[(l(!0),d(L,null,ie(e.cards,(i,s)=>(l(),d("div",{class:"paper-stat-card",key:i?.key},[n("div",{class:T(["paper-stat-value",{"is-pending":!e.stat}])},c(e.stat?e.fmt?.bigNum(i?.total):"\u2014"),3),n("div",zU,c(i?.label),1),e.history[i?.key].bars?.length>1&&e.history[i?.key].deltaToday>0?(l(),d("div",HU," +"+c(e.fmt?.number(e.history[i?.key].deltaToday))+" today ",1)):f("v-if",!0)]))),128))]))],2),f(" Before / after "),t[22]||(t[22]=n("section",{class:"paper-section-head paper-wrap"},[n("div",null,[n("div",{class:"paper-eyebrow"},"Try it"),n("h2",null,[g("The same repository, "),n("em",null,"minus you.")])]),n("p",null," Names, affiliations, emails, and any term you list are replaced across the README, source files, notebooks, and PDF links. Reviewers browse the real thing, not a zip. ")],-1)),n("div",jU,[n("div",GU,[t[13]||(t[13]=n("label",{for:"demo-terms"},[g("Terms to redact "),n("span",null,[g("comma-separated"),n("span",{class:"dt-try"},", try adding your own")])],-1)),P(n("textarea",{id:"demo-terms",class:"demo-terms-input",rows:"1","onUpdate:modelValue":t[0]||(t[0]=i=>e.demoTerms=i),spellcheck:"false",autocomplete:"off",onKeydown:t[1]||(t[1]=mt(ce(()=>{},["prevent"]),["enter"]))},null,544),[[dt,e.demoTerms]])]),n("div",WU,[n("div",KU,[t[15]||(t[15]=n("div",{class:"pane-header"},[n("span",{class:"pane-label"},[g("Your repository "),n("span",{class:"pane-edit-hint"},"\xB7 editable")]),n("span",{class:"pane-url"},"github.com/jane-smith/DeepLearnUtils")],-1)),n("div",YU,[n("div",XU,[(l(!0),d(L,null,ie(e.demoParts(),(i,s)=>(l(),d(L,{key:s},[i.redacted?(l(),d("mark",JU,c(i.original),1)):(l(),d(L,{key:1},[g(c(i.text),1)],64))],64))),128)),t[14]||(t[14]=g("\u200B",-1))]),P(n("textarea",{class:"pane-body demo-input","onUpdate:modelValue":t[2]||(t[2]=i=>e.demoText=i),spellcheck:"false","aria-label":"Sample README to anonymize"},null,512),[[dt,e.demoText]])])]),n("div",QU,[n("div",ZU,[t[16]||(t[16]=n("span",{class:"pane-label"},"What reviewers see",-1)),n("span",xU,[n("span",eF,c(e.demoCount())+" redacted",1)])]),n("div",tF,[(l(!0),d(L,null,ie(e.demoParts(),(i,s)=>(l(),d(L,{key:s},[i.redacted?(l(),d("span",{key:0,class:"redact-chip",title:"Replaces \u201C"+i.original+"\u201D"},c(i.text),9,nF)):(l(),d(L,{key:1},[g(c(i.text),1)],64))],64))),128))])])])]),f(" What you get: one screenshot, three tabs "),t[23]||(t[23]=n("section",{class:"paper-section-head paper-wrap",id:"about"},[n("div",null,[n("div",{class:"paper-eyebrow"},"What you get"),n("h2",null,"Anonymize, review, manage.")]),n("p",null," Terms are redacted in text files. Enabled images and PDFs are served unchanged, so review them for identifying content before sharing. Monitor your mirror, update it, or remove it when the review is over. ")],-1)),n("section",oF,[n("div",sF,[(l(!0),d(L,null,ie(e.features,(i,s)=>(l(),d("div",{class:T(["paper-feature",{"is-on":e.feature===i?.key}]),key:i?.key},[n("button",{type:"button",class:"paper-feature-tab",role:"tab",onClick:r=>e.selectFeature(i.key),onKeydown:r=>e.featureKeydown(r,s),id:"feature-tab-"+i?.key,"aria-controls":"feature-panel-"+i?.key,"aria-selected":e.feature===i?.key,tabindex:e.feature===i?.key?0:-1},[n("span",rF,[n("span",aF,c(i?.num),1),t[17]||(t[17]=n("span",{class:"pf-sep"}," \xB7 ",-1)),n("span",lF,c(i?.eyebrow),1)]),n("span",dF,[g(c(i?.title.split("-")[0]),1),i?.title.includes("-")?(l(),d("span",uF,"-")):f("v-if",!0),g(c(i?.title.split("-").slice(1).join("-"))+" "+c(i?.accent),1)])],40,iF),n("div",cF,[n("span",pF,[g(c(i?.title.split("-")[0]),1),i?.title.includes("-")?(l(),d("span",fF,"-")):f("v-if",!0),g(c(i?.title.split("-").slice(1).join("-"))+" "+c(i?.accent),1)]),n("p",null,c(i?.text),1),n("a",{class:"paper-link-arrow",href:e.safeUrl(e.featureHref(i)),target:e.featureTarget(i)},c(i?.cta)+" \u2192",9,hF)])],2))),128))]),n("div",mF,[(l(!0),d(L,null,ie(e.features,(i,s)=>P((l(),d("div",{class:"paper-feature-panel",role:"tabpanel",key:i?.key,id:"feature-panel-"+i?.key,"aria-labelledby":"feature-tab-"+i?.key},[n("div",vF,[n("div",yF,[t[18]||(t[18]=n("span",{class:"dots"},[n("i"),n("i"),n("i")],-1)),n("span",bF,c(i?.url),1)]),n("div",{class:T(["feat-mock","is-"+i?.key]),role:"img","aria-label":i?.alt},[i?.key==="anonymize"?(l(),d("div",kF,[...t[19]||(t[19]=[Ce('<div class="fm-label">GitHub re
Line truncated
|
||||
<span class="ln">2</span><span class="c"># Author: <span class="redact">XXXX-1</span>, <span class="redact">XXXX-2</span></span>
|
||||
<span class="ln">3</span><span class="k">import</span> torch
|
||||
<span class="ln">4</span><span class="k">from</span> utils <span class="k">import</span> load_config
|
||||
@@ -47,15 +54,14 @@ python experiments/run.py`),n("p",null,[y("Contact: "),n("span",{class:"redact",
|
||||
<span class="ln">7</span> model = build_model(config)
|
||||
<span class="ln">8</span> <span class="k">for</span> epoch <span class="k">in</span> range(config.epochs):
|
||||
<span class="ln">9</span> loss = step(model)
|
||||
<span class="ln">10</span> log(<span class="s">"epoch %d: %.3f"</span>, epoch, loss)</pre></div>`,2)])])):(l(),d("div",Kq,[...t[17]||(t[17]=[ke('<div class="fm-dash-head"><span>Project</span><span>Status</span><span>Views</span><span>Expires</span></div><div class="fm-dash-row"><span><b>DeepLearnUtils</b><i>paper-7f2a \xB7 ICSE 2027</i></span><span class="st is-ready">Ready</span><span>41</span><span>Mar 31, 2027</span></div><div class="fm-dash-row"><span><b>vision-bench#212</b><i>pull request \xB7 FSE 2027</i></span><span class="st is-ready">Ready</span><span>12</span><span>Never</span></div><div class="fm-dash-row"><span><b>graph-repair</b><i>gr-91ab</i></span><span class="st is-progress">Updating</span><span>5</span><span>Jan 5, 2027</span></div><div class="fm-dash-row is-muted"><span><b>JarSift</b><i>OOPSLA 2025</i></span><span class="st is-expired">Expired</span><span>72</span><span>Sep 28, 2026</span></div>',5)])]))],10,jq)])],8,Fq)),[[J,e.feature===o?.key]])),128))])]),f(" FAQ teaser "),t[27]||(t[27]=ke('<section class="paper-qa paper-wrap" aria-labelledby="qa-heading"><div><div class="paper-eyebrow">Before you sign in</div><h2 id="qa-heading">The questions that come up most.</h2><a class="paper-link-arrow" href="/faq">Read the full FAQ \u2192</a></div><div><div class="q"><a href="/faq#permissions">What access does GitHub sign-in require?</a><p> The GitHub App has read-only access to gists and the repositories you choose. Legacy OAuth uses broader permissions. Anonymous GitHub never writes to your repositories. </p></div><div class="q"><a href="/faq#formats">Which file formats are supported?</a><p> Source code with syntax highlighting, Markdown, Jupyter notebooks, PDFs, images, and GitHub Pages sites. Text files are anonymized; enabled images and PDFs are served unchanged. </p></div><div class="q"><a href="/faq#expiration">What happens when an anonymized repository expires?</a><p> You choose: the mirror is removed, or visitors are redirected to the original GitHub repository once the review is over. </p></div></div></section><section class="paper-cta paper-wrap" aria-labelledby="cta-heading"><div><h2 id="cta-heading">Ready for review?</h2><p>Create an anonymous mirror in a couple of minutes. Reviewers just open the link.</p></div><div class="paper-cta-actions"><a class="btn-hero" href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a><a class="paper-link-arrow" href="/faq">Read the FAQ \u2192</a></div></section>',2)),n("footer",Yq,[n("div",Xq,[t[21]||(t[21]=n("div",{class:"paper-footer-brand"},[n("div",{class:"paper-footer-mark"},[y("Anonymous "),n("em",null,"GitHub")]),n("p",{class:"paper-footer-tag"}," A read-only, identity-stripped mirror of your repository, built for double-anonymous peer review. ")],-1)),n("div",Jq,[t[18]||(t[18]=n("div",{class:"paper-footer-head"},"Product",-1)),t[19]||(t[19]=n("a",{href:"/anonymize"},"Anonymize",-1)),e.user?(l(),d("a",Qq,"My work")):f("v-if",!0),e.user?(l(),d("a",Zq,"Conferences")):f("v-if",!0),t[20]||(t[20]=n("a",{href:"/faq"},"FAQ",-1))]),t[22]||(t[22]=ke('<div class="paper-footer-col"><div class="paper-footer-head">Project</div><a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a><a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a><a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Support</a></div><div class="paper-footer-col"><div class="paper-footer-head">Contact</div><a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a><a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a></div>',2))]),t[24]||(t[24]=n("div",{class:"paper-footer-rule"},null,-1)),n("div",xq,[n("span",null,"\xA9 "+c(e.year||2026)+" Anonymous GitHub \xB7 GPL-3.0 licensed",1),t[23]||(t[23]=n("span",{class:"paper-footer-meta"},"Built for reviewers, by researchers.",-1))])])])}var e$={class:"paper-empty"},t$={class:"paper-empty-inner"},n$={key:0,class:"paper-eyebrow"},o$={key:1,class:"paper-eyebrow"},s$={key:2,class:"paper-empty-title"},i$=["textContent"];function Oc(e,t){return l(),d("div",e$,[n("div",t$,[e.error?f("v-if",!0):(l(),d("div",n$,"Working")),e.error?(l(),d("div",o$,"Error")):f("v-if",!0),e.error?f("v-if",!0):(l(),d("h1",s$,[...t[0]||(t[0]=[y(" Loading",-1),n("span",{class:"dot-anim"},"\u2026",-1)])])),e.error?(l(),d("h1",{key:3,class:"paper-empty-title",textContent:c(e.fmt.translate(e.error))},null,8,i$)):f("v-if",!0)])])}var r$={class:"container paper-page paper-settings"},a$={class:"paper-crumbs"},l$={class:"here"},d$={class:"paper-page-title"},u$={class:"paper-settings-body"},c$={class:"paper-settings-toc"},p$={href:"#conf-billing"},f$={class:"form needs-validation paper-settings-main",name:"conference",novalidate:""},h$={id:"conf-basics",class:"paper-settings-section"},m$={class:"form-group"},g$={class:"invalLine truncated
|
||||
`).length))+" lines",1)):f("v-if",!0),e.type=="html"&&e.fileSize(e.filePath)!=null?(l(),d("span",qL,c(e.formatSize(e.fileSize(e.filePath))),1)):f("v-if",!0),e.redactionCount()?(l(),d("b",$L,c(e.redactionCount())+" redacted",1)):f("v-if",!0)]),A(n("button",{type:"button",class:"btn btn-sm btn-icon-only btn-ghost copy-path-btn","aria-label":e.pathCopied?"Path copied":"Copy file path",title:e.pathCopied?"Path copied":"Copy file path",onClick:t[2]||(t[2]=a=>e.copyPath())},[(l(),d("svg",UL,[n("use",{href:e.pathCopied?"#ui-icon-check":"#ui-icon-link"},null,8,FL)]))],8,LL),[[J,e.filePath]])]),e.type!="html"&&typeof e.content=="string"?(l(),d("span",BL,[e.languageName?(l(),d(B,{key:0},[y(c(e.languageName),1),t[37]||(t[37]=n("span",{class:"sep"},"\xB7",-1))],64)):f("v-if",!0),y(c(e.fmt?.number(e.content.split(`
|
||||
`).length))+" lines",1),n("span",zL,[t[38]||(t[38]=n("span",{class:"sep"},"\xB7",-1)),y(c(e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)]),e.redactionCount()?(l(),d(B,{key:1},[t[39]||(t[39]=n("span",{class:"sep"},"\xB7",-1)),n("span",HL,c(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("span",jL,[t[42]||(t[42]=y("Markdown",-1)),e.fileSize(e.filePath)!=null?(l(),d(B,{key:0},[t[40]||(t[40]=n("span",{class:"sep"},"\xB7",-1)),y(c(e.formatSize(e.fileSize(e.filePath))),1)],64)):f("v-if",!0),e.redactionCount()?(l(),d(B,{key:1},[t[41]||(t[41]=n("span",{class:"sep"},"\xB7",-1)),n("span",GL,c(e.redactionCount())+" redacted",1)],64)):f("v-if",!0)])):f("v-if",!0),n("div",WL,[e.type!="html"&&e.symbols().length>1?(l(),d("div",KL,[n("button",{type:"button",class:P(["btn btn-sm btn-ghost btn-symbols",{"is-on":e.symbolsOpen}]),"aria-expanded":e.symbolsOpen?"true":"false","aria-haspopup":"true",title:"Symbols in this file",onClick:t[3]||(t[3]=a=>e.symbolsOpen=!e.symbolsOpen)},[...t[43]||(t[43]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-list"})],-1),n("span",{class:"btn-find-label"},"Symbols",-1)])],10,YL),e.symbolsOpen?(l(),d("div",XL,[n("div",JL,c(e.symbols().length)+" "+c(e.symbols().length===1?"symbol":"symbols"),1),(l(!0),d(B,null,ie(e.symbols(),a=>(l(),d("button",{type:"button",class:"symbols-item",role:"menuitem",key:a.line+a.name,onClick:u=>e.goToSymbol(a.line)},[n("span",{class:P(["symbols-kind","is-"+a.kind])},c(a.kind==="class"?"C":a.kind==="function"?"\u0192":"v"),3),n("span",ZL,c(a.name),1),n("span",xL,c(a.line),1)],8,QL))),128))])):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("button",{key:1,type:"button",class:P(["btn btn-sm btn-ghost btn-icon-only btn-find btn-md-find",{"is-on":e.mdFindOpen}]),"aria-label":"Find on this page",title:"Find on this page ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[4]||(t[4]=a=>e.toggleMdFind())},[...t[44]||(t[44]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],10,eU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:2,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-find","aria-label":"Find in file",title:"Find in file ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[5]||(t[5]=a=>e.findInFile())},[...t[45]||(t[45]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],8,tU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:3,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-goto","aria-label":"Go to line",title:"Go to line ("+(e.isMac?"\u2318":"Ctrl+")+"G)",onClick:t[6]||(t[6]=a=>e.gotoLine())},[...t[46]||(t[46]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1)])],8,nU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:4,type:"button",class:P(["btn btn-sm btn-icon-only btn-ghost btn-toggle",{"is-on":e.wrapLines}]),"aria-pressed":e.wrapLines?"true":"false","aria-label":"Wrap long lines",title:"Wrap long lines (Alt+Z)",onClick:t[7]||(t[7]=a=>e.toggleWrap())},[...t[47]||(t[47]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)])],10,oU)):f("v-if",!0),n("div",sU,[n("a",{target:"_self",class:"tb-seg-btn",title:e.type=="html"?"Open the Markdown source":"Open the raw file",href:e.safeUrl(e.url)},"Raw",8,iU),typeof e.content=="string"?(l(),d("button",{key:0,type:"button",class:"tb-seg-btn tb-seg-icon","aria-label":e.contentCopied?"Copied":"Copy file contents",title:e.contentCopied?"Copied":"Copy file contents",onClick:t[8]||(t[8]=a=>e.copyContent())},[(l(),d("svg",aU,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,lU)]))],8,rU)):f("v-if",!0),n("a",{target:"_self",class:"tb-seg-btn tb-seg-icon","aria-label":"Download file",title:"Download file",href:e.safeUrl(e.url+"&download=true")},[...t[48]||(t[48]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1)])],8,dU)]),typeof e.content=="string"?(l(),d("button",{key:5,type:"button",class:"btn btn-sm btn-icon-only btn-ghost tb-copy-m","aria-label":e.contentCopied?"Copied":"Copy file contents",onClick:t[9]||(t[9]=a=>e.copyContent())},[(l(),d("svg",cU,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,pU)]))],8,uU)):f("v-if",!0),e.type=="html"&&e.readmeToc().length>=3?(l(),d("div",fU,[n("button",{type:"button",class:P(["btn btn-sm btn-icon-only btn-ghost tb-toc-btn",{"is-on":e.tocOpen}]),"aria-label":"On this page",title:"On this page","aria-haspopup":"true","aria-expanded":e.tocOpen?"true":"false",onClick:t[10]||(t[10]=ce(a=>{e.tocOpen=!e.tocOpen,e.moreOpen=!1},["stop"]))},[...t[49]||(t[49]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-outline"})],-1)])],10,hU),e.tocOpen?(l(),d("div",{key:0,class:"tb-menu tb-toc-menu",role:"menu",onClick:t[12]||(t[12]=a=>e.tocOpen=!1)},[t[50]||(t[50]=n("div",{class:"tb-menu-label"},"On this page",-1)),n("button
Line truncated
|
||||
`).length))+" lines",1)],64)):f("v-if",!0),y(" \xB7 "+c(e.type=="html"&&e.fileSize(e.filePath)!=null?e.formatSize(e.fileSize(e.filePath)):e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)])):f("v-if",!0),e.options?.publishedAt||e.options?.sourceCommitDate||e.options?.anonymizedAt||e.options?.lastUpdateDate?(l(),d("div",kU)):f("v-if",!0),e.type=="html"?(l(),d("button",{key:3,type:"button",role:"menuitem",onClick:t[14]||(t[14]=a=>e.toggleMdFind())},[...t[53]||(t[53]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type=="html"?(l(),d("div",CU)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:5,type:"button",role:"menuitem",onClick:t[15]||(t[15]=a=>e.findInFile())},[...t[54]||(t[54]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),y("Find in file",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:6,type:"button",role:"menuitem",onClick:t[16]||(t[16]=a=>e.gotoLine())},[...t[55]||(t[55]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1),y("Go to line",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:7,type:"button",role:"menuitemcheckbox","aria-checked":e.wrapLines?"true":"false",onClick:t[17]||(t[17]=a=>e.toggleWrap())},[t[56]||(t[56]=n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)),t[57]||(t[57]=y("Wrap lines",-1)),e.wrapLines?(l(),d("span",SU,"\u2713")):f("v-if",!0)],8,EU)):f("v-if",!0),e.type!="html"?(l(),d("div",NU)):f("v-if",!0),e.type=="html"?(l(),d("a",{key:9,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[58]||(t[58]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,RU)):f("v-if",!0),e.type!="html"?(l(),d("a",{key:10,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[59]||(t[59]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),y("View raw",-1)])],8,DU)):f("v-if",!0),n("a",{role:"menuitem",target:"_self",href:e.safeUrl(e.url+"&download=true")},[...t[60]||(t[60]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1),y("Download file",-1)])],8,AU),e.options?.hasWebsite?(l(),d("a",{key:11,role:"menuitem",target:"_self",href:e.safeUrl("/w/"+e.repoId+"/")},[...t[61]||(t[61]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-globe"})],-1),y("Open website",-1)])],8,TU)):f("v-if",!0),e.options?.isAdmin||e.options?.isOwner?(l(),d("a",{key:12,role:"menuitem",href:e.safeUrl("/anonymize/"+e.repoId)},[...t[62]||(t[62]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-edit"})],-1),y("Edit anonymization",-1)])],8,PU)):f("v-if",!0),e.options?.download?(l(),d("a",{key:13,role:"menuitem",target:"_self",href:e.safeUrl("/api/repo/"+e.repoId+"/zip")},[...t[63]||(t[63]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-archive"})],-1),y("Download ZIP",-1)])],8,_U)):f("v-if",!0),n("button",{type:"button",role:"menuitem",onClick:t[18]||(t[18]=a=>e.copyPath())},[...t[64]||(t[64]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-link"})],-1),y("Copy file path",-1)])]),t[65]||(t[65]=n("div",{class:"tb-menu-sep"},null,-1)),t[66]||(t[66]=n("a",{role:"menuitem",href:"/faq"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-help"})]),y("FAQ")],-1)),t[67]||(t[67]=n("a",{role:"menuitem",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",target:"_blank",rel:"noopener"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-bug"})]),y("Report a bug")],-1))])):f("v-if",!0)])]),e.gotoOpen&&e.type!="html"?(l(),d("form",{key:2,class:"goto-pop",role:"dialog","aria-label":"Go to line",onSubmit:t[22]||(t[22]=ce(a=>e.submitGoto(),["prevent"]))},[t[68]||(t[68]=n("label",{class:"goto-label",for:"goto-input"},"Go to line",-1)),A(n("input",{id:"goto-input",class:"goto-input",type:"text",inputmode:"numeric",autocomplete:"off",spellcheck:"false",placeholder:"1\u2013"+e.fmt?.number(typeof e.content=="string"?e.content.split(`
|
||||
`).length:1),"aria-invalid":e.gotoError?"true":"false","onUpdate:modelValue":t[20]||(t[20]=a=>e.gotoValue=a),onKeydown:t[21]||(t[21]=At(ce(a=>e.closeGoto(),["prevent","stop"]),["esc"]))},null,40,OU),[[ct,e.gotoValue]]),n("span",{class:P(["goto-hint",{"is-error":e.gotoError}])},c(e.gotoError||"Line, or line:column"),3)],32)):f("v-if",!0)]),e.type!="html"?A((l(),d("div",IU,null,512)),[[r,{content:e.content,options:e.aceOption}]]):f("v-if",!0),e.type=="html"&&e.mdFindOpen?(l(),d("div",MU,[n("input",{type:"search",class:"md-find-input",placeholder:"Find on this page","aria-label":"Find on this page",value:e.mdFindQuery,onInput:t[23]||(t[23]=a=>{e.mdFindQuery=a.target.value,e.runMdFind()}),onKeydown:[t[24]||(t[24]=At(ce(a=>a.shiftKey?e.stepMdFind(-1):e.stepMdFind(1),["prevent"]),["enter"])),t[25]||(t[25]=At(ce(a=>e.toggleMdFind(!1),["prevent"]),["esc"]))]},null,40,VU),n("span",qU,[n("button",{type:"button","aria-label":"Previous match",onClick:t[26]||(t[26]=a=>e.stepMdFind(-1)),disabled:!e.mdFindTotal},[...t[69]||(t[69]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-left"})],-1)])],8,$U),n("button",{type:"button","aria-label":"Next match",onClick:t[27]||(t[27]=a=>e.stepMdFind(1)),disabled:!e.mdFindTotal},[...t[70]||(t[70]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-right"})],-1)])],8,LU)]),n("span",UU,c(e.mdFindQuery?e.mdFindTotal?e.mdFindIndex+1+" of "+e.mdFindTotal:"No results":""),1),n("button",{type:"button",class:P(["md-find-opt",{"is-on":e.mdFindCase}]),"aria-pressed":e.mdFindCase?"true":"false",title:"Match case","aria-label":"Match case",onClick:t[28]||(t[28]=a=>{e.mdFindCase=!e.mdFindCase,e.runMdFind()})},"Aa",10,FU),n("button",{type:"button",class:"md-find-close","aria-label":"Close find",onClick:t[29]||(t[29]=a=>e.toggleMdFind(!1))},"\xD7")])):f("v-if",!0),e.type=="html"?(l(),d("div",BU,[n("div",{class:"file-content markdown-body",innerHTML:e.renderReadme(e.sanitize(e.content))},null,8,zU),e.readmeToc().length>=3?(l(),d("aside",HU,[t[71]||(t[71]=n("div",{class:"readme-toc-head"},"On this page",-1)),n("nav",jU,[n("a",{href:"#",class:P(["lvl-2 toc-top",{"is-active":!e.activeHeading}]),onClick:t[30]||(t[30]=ce(a=>e.scrollToHeading(null),["prevent"]))},"Introduction",2),(l(!0),d(B,null,ie(e.readmeToc(),(a,u)=>(l(),d("a",{key:a.id,href:"#"+a.id,class:P(["lvl-"+a.level,{"is-active":e.activeHeading===a.id}]),onClick:ce(p=>e.scrollToHeading(a.id),["prevent"])},c(a.text),11,GU))),128))])])):f("v-if",!0)])):f("v-if",!0)],2)):f("v-if",!0),e.type=="html-doc"&&!e.showSource&&e.content!=null?(l(),sn(o,{key:2,content:e.content,"base-url":e.fileBaseUrl,"allow-scripts":e.allowScripts},null,8,["content","base-url","allow-scripts"])):f("v-if",!0),e.type=="image"?(l(),d("img",{key:3,class:"image-content",src:e.safeUrl(e.url)},null,8,WU)):f("v-if",!0),e.type=="media"?(l(),d("iframe",{key:4,class:"h-100 overflow-auto w-100 b-0",src:e.safeUrl(e.url)},null,8,KU)):f("v-if",!0),e.type=="pdf"?(l(),sn(i,{key:5,src:e.safeUrl(e.url)},null,8,["src"])):f("v-if",!0),e.type=="audio"?(l(),d("div",YU,[n("audio",XU,[n("source",{src:e.safeUrl(e.url)},null,8,JU)])])):f("v-if",!0),e.type=="IPython"?(l(),d("div",QU,[Ue(s,{file:e.url,content:e.content},null,8,["file","content"])])):f("v-if",!0),e.type=="rate_limited"?(l(),d("div",ZU,[n("div",xU,[t[74]||(t[74]=ke('<div class="paper-ratelimit-head"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg><div><div class="paper-error-eyebrow">Temporarily paused</div><div class="paper-error-title">GitHub API rate limit reached</div></div></div>',1)),n("p",eF,[t[72]||(t[72]=y("This repository will be available in ",-1)),n("strong",null,c(e.rateLimitCountdown),1),t[73]||(t[73]=y(". The page will reload automatically.",-1))])])])):f("v-if",!0),e.type=="error"?(l(),d("div",tF,[n("section",nF,[n("h1",oF,c(e.fmt.translate(e.isSourceAccessError()?"VIEWER_ERRORS.access_title":"VIEWER_ERRORS.title")),1),n("p",null,c(e.fmt.translate("ERRORS."+e.content)),1),e.isSourceAccessError()?(l(),d(B,{key:0},[n("p",null,c(e.fmt.translate("VIEWER_ERRORS.reader_help")),1),n("p",null,c(e.fmt.translate("VIEWER_ERRORS.owner_help")),1)],64)):f("v-if",!0),n("div",sF,[e.isSourceAccessError()?f("v-if",!0):(l(),d("a",iF,"Back to home")),e.isSourceAccessError()?f("v-if",!0):(l(),d("a",rF,"Read the FAQ")),n("button",{type:"button",class:"btn btn-primary",onClick:t[31]||(t[31]=a=>e.retryContent())},c(e.fmt.translate("VIEWER_ERRORS.retry")),1),e.isSourceAccessError()?(l(),d("a",aF,c(e.fmt.translate("VIEWER_ERRORS.dashboard")),1)):f("v-if",!0)])])])):f("v-if",!0),e.type=="loading"&&!e.error?(l(),d("section",lF,[n("div",dF,[n("strong",null,c(e.fmt.translate(e.fileReceivedBytes?"VIEWER_LOADING.receiving":"VIEWER_LOADING.loading")),1),n("span",uF,c(e.filePath),1),e.fileLoadingSlow?(l(),d("p",cF,c(e.fmt.translate("VIEWER_LOADING.slow")),1)):f("v-if",!0)]),n("div",pF,[(l(),d(B,null,ie(10,a=>n("span",{key:a})),64))])])):f("v-if",!0),e.type==
Line truncated
|
||||
<span class="ln">10</span> log(<span class="s">"epoch %d: %.3f"</span>, epoch, loss)</pre></div>`,2)])])):(l(),d("div",EF,[...t[21]||(t[21]=[Ce('<div class="fm-dash-head"><span>Project</span><span>Status</span><span>Views</span><span>Expires</span></div><div class="fm-dash-row"><span><b>DeepLearnUtils</b><i>paper-7f2a \xB7 ICSE 2027</i></span><span class="st is-ready">Ready</span><span>41</span><span>Mar 31, 2027</span></div><div class="fm-dash-row"><span><b>vision-bench#212</b><i>pull request \xB7 FSE 2027</i></span><span class="st is-ready">Ready</span><span>12</span><span>Never</span></div><div class="fm-dash-row"><span><b>graph-repair</b><i>gr-91ab</i></span><span class="st is-progress">Updating</span><span>5</span><span>Jan 5, 2027</span></div><div class="fm-dash-row is-muted"><span><b>JarSift</b><i>OOPSLA 2025</i></span><span class="st is-expired">Expired</span><span>72</span><span>Sep 28, 2026</span></div>',5)])]))],10,wF)])],8,gF)),[[ee,e.feature===i?.key]])),128))])]),f(" FAQ teaser "),t[24]||(t[24]=Ce('<section class="paper-qa paper-qa-r3 paper-wrap" aria-labelledby="qa-heading"><div class="qa-head"><div><div class="paper-eyebrow">Before you sign in</div><h2 id="qa-heading">The questions that come up most.</h2></div><a class="paper-link-arrow" href="/faq">Read the full FAQ \u2192</a></div><div class="qa-grid"><div class="q"><a href="/faq#permissions">What access does GitHub sign-in require?</a><p> The GitHub App has read-only access to gists and the repositories you choose. Legacy OAuth uses broader permissions. Anonymous GitHub never writes to your repositories. </p></div><div class="q"><a href="/faq#formats">Which file formats are supported?</a><p> Source code with syntax highlighting, Markdown, Jupyter notebooks, PDFs, images, and GitHub Pages sites. Text files are anonymized; enabled images and PDFs are served unchanged. </p></div><div class="q"><a href="/faq#expiration">What happens when an anonymized repository expires?</a><p> You choose: the mirror is removed, or visitors are redirected to the original GitHub repository once the review is over. </p></div></div></section><section class="paper-cta paper-wrap" aria-labelledby="cta-heading"><div><h2 id="cta-heading">Ready for review?</h2><p>Create an anonymous mirror in a couple of minutes. Reviewers just open the link.</p></div><div class="paper-cta-actions"><a class="btn-hero" href="/signin" target="_self"><i class="fab fa-github" aria-hidden="true"></i> Sign in with GitHub</a><a class="paper-link-arrow" target="_self" href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/">See an anonymized repository \u2192</a></div></section>',2)),Me(o,{name:"partials/footer.htm",state:e.viewState},null,8,["state"])])}var SF={class:"paper-empty"},NF={class:"paper-empty-inner"},RF={key:0,class:"paper-eyebrow"},DF={key:1,class:"paper-eyebrow"},AF={key:2,class:"paper-empty-title"},TF=["textContent"];function Qc(e,t){return l(),d("div",SF,[n("div",NF,[e.error?f("v-if",!0):(l(),d("div",RF,"Working")),e.error?(l(),d("div",DF,"Error")):f("v-if",!0),e.error?f("v-if",!0):(l(),d("h1",AF,[...t[0]||(t[0]=[g(" Loading",-1),n("span",{class:"dot-anim"},"\u2026",-1)])])),e.error?(l(),d("h1",{key:3,class:"paper-empty-title",textContent:c(e.fmt.translate(e.error))},null,8,TF)):f("v-if",!0)])])}var PF={class:"container paper-page paper-settings"},OF={class:"paper-crumbs"},IF={class:"here"},_F={class:"paper-page-title"},MF={class:"paper-settings-body"},VF={class:"paper-settings-toc"},qF={href:"#conf-billing"},$F={class:"form needs-validation paper-settings-main",name:"conference",novalidate:""},LF={id:"conf-basics",class:"paper-settings-section"},UF={class:"form-group"},FF={class:"invalid-feedback"},BF={class:"invalid-feedback"},zF={class:"form-group"},HF={class:"invalid-feedback"},jF={class:"form-group"},GF={class:"invalid-feedback"},WF={class:"invalid-feedback"},KF={id:"conf-window",class:"paper-settings-section"},YF={class:"form-grid-2"},XF={class:"form-group"},JF={class:"invalid-feedback"},QF={class:"invalid-feedback"},ZF={class:"form-group"},xF={class:"invalid-feedback"},e7={class:"invalid-feedback"},t7={id:"conf-rendering",class:"paper-settings-section"},n7={class:"form-check"},o7={class:"form-check-input",type:"checkbox",id:"link",name:"link"},s7={class:"form-check"},i7={class:"form-check-input",type:"checkbox",id:"image",name:"image"},r7={class:"form-check"},a7={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},l7={class:"form-check"},d7={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},u7={id:"conf-features",class:"paper-settings-section"},c7={class:"form-check"},p7={class:"form-check-input",type:"checkbox",id:"update",name:"update"},f7={class:"form-check"},h7={class:"form-check-input",type:"checkbox",id:"page",name:"page"},m7={id:"conf-plan",class:"paper-settings-section"},g7={key:0,class:"paper-section-copy"},v7={class:"conference-tiers",role:"radiogroup","aria-label":"Review package size"}Line truncated
|
||||
`).length))+" lines",1)):f("v-if",!0)]),P(n("button",{type:"button",class:"btn btn-sm btn-icon-only btn-ghost copy-path-btn","aria-label":e.pathCopied?"Path copied":"Copy file path",title:e.pathCopied?"Path copied":"Copy file path",onClick:t[2]||(t[2]=a=>e.copyPath())},[(l(),d("svg",u9,[n("use",{href:e.pathCopied?"#ui-icon-check":"#ui-icon-link"},null,8,c9)]))],8,d9),[[ee,e.filePath]])]),e.type!="html"&&typeof e.content=="string"?(l(),d("span",p9,[e.redactionCount()?(l(),d(L,{key:0},[n("span",{class:"meta-redact",title:e.redactionCount()+" redacted terms"},[g(c(e.redactionCount()),1),t[47]||(t[47]=n("span",{class:"meta-redact-word"}," redacted",-1))],8,f9),t[48]||(t[48]=n("span",{class:"sep"},"\xB7",-1))],64)):f("v-if",!0),g(c(e.languageName||"Text"),1),n("span",h9,[t[49]||(t[49]=n("span",{class:"sep"},"\xB7",-1)),g(c(e.content.length<1024?e.content.length+" B":(e.content.length/1024).toFixed(1)+" KB"),1)]),t[50]||(t[50]=n("span",{class:"sep"},"\xB7",-1)),g(c(e.fmt?.number(e.content.split(`
|
||||
`).length))+" lines",1)])):f("v-if",!0),e.type=="html"?(l(),d("span",m9,[e.redactionCount()?(l(),d(L,{key:0},[n("span",{class:"meta-redact",title:e.redactionCount()+" redacted terms"},[g(c(e.redactionCount()),1),t[51]||(t[51]=n("span",{class:"meta-redact-word"}," redacted",-1))],8,g9),t[52]||(t[52]=n("span",{class:"sep"},"\xB7",-1))],64)):f("v-if",!0),t[54]||(t[54]=g("Markdown",-1)),e.fileSize(e.filePath)!=null?(l(),d(L,{key:1},[t[53]||(t[53]=n("span",{class:"sep"},"\xB7",-1)),g(c(e.formatSize(e.fileSize(e.filePath))),1)],64)):f("v-if",!0)])):f("v-if",!0),n("div",v9,[e.isCsv()?(l(),d("div",y9,[n("button",{type:"button",role:"radio",class:T(["seg-chip",{active:e.csvView==="table"}]),"aria-checked":e.csvView==="table",onClick:t[3]||(t[3]=a=>e.csvView="table")},"Table",10,b9),n("button",{type:"button",role:"radio",class:T(["seg-chip",{active:e.csvView==="raw"}]),"aria-checked":e.csvView==="raw",onClick:t[4]||(t[4]=a=>e.csvView="raw"),title:"Show the file as text with line numbers"},"Text",10,w9)])):f("v-if",!0),e.type!="html"&&e.symbols().length>1?(l(),d("div",k9,[n("button",{type:"button",class:T(["btn btn-sm btn-ghost btn-symbols",{"is-on":e.symbolsOpen}]),"aria-expanded":e.symbolsOpen?"true":"false","aria-haspopup":"true",title:"Symbols in this file",onClick:t[5]||(t[5]=a=>e.symbolsOpen=!e.symbolsOpen)},[...t[55]||(t[55]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-list"})],-1),n("span",{class:"btn-find-label"},"Symbols",-1)])],10,C9),e.symbolsOpen?(l(),d("div",E9,[n("div",S9,c(e.symbols().length)+" "+c(e.symbols().length===1?"symbol":"symbols"),1),(l(!0),d(L,null,ie(e.symbols(),a=>(l(),d("button",{type:"button",class:"symbols-item",role:"menuitem",key:a.line+a.name,onClick:u=>e.goToSymbol(a.line)},[n("span",{class:T(["symbols-kind","is-"+a.kind])},c(a.kind==="class"?"C":a.kind==="function"?"\u0192":"v"),3),n("span",R9,c(a.name),1),n("span",D9,c(a.line),1)],8,N9))),128))])):f("v-if",!0)])):f("v-if",!0),e.type=="html"?(l(),d("button",{key:2,type:"button",class:T(["btn btn-sm btn-ghost btn-icon-only btn-find btn-md-find",{"is-on":e.mdFindOpen}]),"aria-label":"Find on this page",title:"Find on this page ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[6]||(t[6]=a=>e.toggleMdFind())},[...t[56]||(t[56]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],10,A9)):f("v-if",!0),e.type!="html"&&!(e.isCsv()&&e.csvView==="table")?(l(),d("div",T9,[e.type!="html"?(l(),d("button",{key:0,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-goto","aria-label":"Go to line",title:"Go to line ("+(e.isMac?"\u2318":"Ctrl+")+"G)",onClick:t[7]||(t[7]=a=>e.gotoLine())},[...t[57]||(t[57]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1)])],8,P9)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:1,type:"button",class:T(["btn btn-sm btn-icon-only btn-ghost btn-toggle",{"is-on":e.wrapLines}]),"aria-pressed":e.wrapLines?"true":"false","aria-label":"Wrap lines",title:"Wrap lines (Alt+Z)",onClick:t[8]||(t[8]=a=>e.toggleWrap())},[...t[58]||(t[58]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)])],10,O9)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:2,type:"button",class:"btn btn-sm btn-ghost btn-icon-only btn-find","aria-label":"Find in file",title:"Find in file ("+(e.isMac?"\u2318":"Ctrl+")+"F)",onClick:t[9]||(t[9]=a=>e.findInFile())},[...t[59]||(t[59]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1)])],8,I9)):f("v-if",!0)])):f("v-if",!0),n("div",_9,[n("a",{target:"_self",class:"tb-seg-btn",title:e.type=="html"?"Open the Markdown source":"Open the raw file",href:e.safeUrl(e.url)},"Raw",8,M9),typeof e.content=="string"?(l(),d("button",{key:0,type:"button",class:"tb-seg-btn tb-seg-icon","aria-label":e.contentCopied?"Copied":"Copy file contents",title:e.contentCopied?"Copied":"Copy file contents",onClick:t[10]||(t[10]=a=>e.copyContent())},[(l(),d("svg",q9,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,$9)]))],8,V9)):f("v-if",!0),n("a",{target:"_self",class:"tb-seg-btn tb-seg-icon","aria-label":"Download file",title:"Download file",href:e.safeUrl(e.url+"&download=true")},[...t[60]||(t[60]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1)])],8,L9)]),typeof e.content=="string"?(l(),d("button",{key:4,type:"button",class:"btn btn-sm btn-icon-only btn-ghost tb-copy-m","aria-label":e.contentCopied?"Copied":"Copy file contents",onClick:t[11]||(t[11]=a=>e.copyContent())},[(l(),d("svg",F9,[n("use",{href:e.contentCopied?"#ui-icon-check":"#project-icon-copy"},null,8,B9)]))],8,U9)):f("v-if",!0),e.type=="html"&&e.readmeToc().length>=3?(l(),d("div",z9,[n("button",{type:"button",class:T(["btn btn-sm btn-icon-only btn-ghost tb-toc-btn",{"is-on":e.tocOpen}]),"aria-label":"On this page",title:"On this page","aria-haspopup":"true","aria-expanded":e.tocOpen?"true":"false",onClick:t[12]||(t[12]=ce(a=>{e.tocOpe
Line truncated
|
||||
`).length))+" lines",1)],64)):f("v-if",!0)])):f("v-if",!0),e.options?.publishedAt||e.options?.sourceCommitDate||e.options?.anonymizedAt||e.options?.lastUpdateDate?(l(),d("div",J9)):f("v-if",!0),e.type=="html"?(l(),d("button",{key:3,type:"button",role:"menuitem",onClick:t[16]||(t[16]=a=>e.toggleMdFind())},[...t[65]||(t[65]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),g("Find in file",-1)])])):f("v-if",!0),e.type=="html"?(l(),d("div",Q9)):f("v-if",!0),e.type!="html"?(l(),d("button",{key:5,type:"button",role:"menuitem",onClick:t[17]||(t[17]=a=>e.findInFile())},[...t[66]||(t[66]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-search"})],-1),g("Find in file",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:6,type:"button",role:"menuitem",onClick:t[18]||(t[18]=a=>e.gotoLine())},[...t[67]||(t[67]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-goto"})],-1),g("Go to line",-1)])])):f("v-if",!0),e.type!="html"?(l(),d("button",{key:7,type:"button",role:"menuitemcheckbox","aria-checked":e.wrapLines?"true":"false",onClick:t[19]||(t[19]=a=>e.toggleWrap())},[t[68]||(t[68]=n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-wrap"})],-1)),t[69]||(t[69]=g("Wrap lines",-1)),e.wrapLines?(l(),d("span",x9,"\u2713")):f("v-if",!0)],8,Z9)):f("v-if",!0),e.isCsv()?(l(),d("button",{key:8,type:"button",role:"menuitem",class:"tb-csv-view",onClick:t[20]||(t[20]=a=>{e.csvView=e.csvView==="table"?"raw":"table",e.moreOpen=!1})},[(l(),d("svg",eB,[n("use",{href:e.csvView==="table"?"#ui-icon-code":"#ui-icon-list"},null,8,tB)])),g(c(e.csvView==="table"?"Show as text":"Show as table"),1)])):f("v-if",!0),e.type!="html"?(l(),d("div",nB)):f("v-if",!0),e.type=="html"?(l(),d("a",{key:10,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[70]||(t[70]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),g("View raw",-1)])],8,oB)):f("v-if",!0),e.type!="html"?(l(),d("a",{key:11,role:"menuitem",target:"_self",href:e.safeUrl(e.url)},[...t[71]||(t[71]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-code"})],-1),g("View raw",-1)])],8,sB)):f("v-if",!0),n("a",{role:"menuitem",target:"_self",href:e.safeUrl(e.url+"&download=true")},[...t[72]||(t[72]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-download"})],-1),g("Download file",-1)])],8,iB),e.options?.hasWebsite?(l(),d("a",{key:12,role:"menuitem",target:"_self",href:e.safeUrl("/w/"+e.repoId+"/")},[...t[73]||(t[73]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-globe"})],-1),g("Open website",-1)])],8,rB)):f("v-if",!0),e.options?.isAdmin||e.options?.isOwner?(l(),d("a",{key:13,role:"menuitem",href:e.safeUrl("/anonymize/"+e.repoId)},[...t[74]||(t[74]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-edit"})],-1),g("Edit anonymization",-1)])],8,aB)):f("v-if",!0),e.options?.download?(l(),d("a",{key:14,role:"menuitem",target:"_self",href:e.safeUrl("/api/repo/"+e.repoId+"/zip")},[...t[75]||(t[75]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-archive"})],-1),g("Download ZIP",-1)])],8,lB)):f("v-if",!0),n("button",{type:"button",role:"menuitem",onClick:t[21]||(t[21]=a=>e.copyPath())},[...t[76]||(t[76]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-link"})],-1),g("Copy file path",-1)])]),t[77]||(t[77]=n("div",{class:"tb-menu-sep"},null,-1)),t[78]||(t[78]=n("a",{role:"menuitem",href:"/faq"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-help"})]),g("FAQ")],-1)),t[79]||(t[79]=n("a",{role:"menuitem",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",target:"_blank",rel:"noopener"},[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-bug"})]),g("Report a bug")],-1))])):f("v-if",!0)])]),e.gotoOpen&&e.type!="html"?(l(),d("form",{key:2,class:"goto-pop",role:"dialog","aria-label":"Go to line",onSubmit:t[25]||(t[25]=ce(a=>e.submitGoto(),["prevent"]))},[t[80]||(t[80]=n("label",{class:"goto-label",for:"goto-input"},"Go to line",-1)),P(n("input",{id:"goto-input",class:"goto-input",type:"text",inputmode:"numeric",autocomplete:"off",spellcheck:"false",placeholder:"1\u2013"+e.fmt?.number(typeof e.content=="string"?e.content.split(`
|
||||
`).length:1),"aria-invalid":e.gotoError?"true":"false","onUpdate:modelValue":t[23]||(t[23]=a=>e.gotoValue=a),onKeydown:t[24]||(t[24]=mt(ce(a=>e.closeGoto(),["prevent","stop"]),["esc"]))},null,40,dB),[[dt,e.gotoValue]]),n("span",{class:T(["goto-hint",{"is-error":e.gotoError}])},c(e.gotoError||"Line, or line:column"),3)],32)):f("v-if",!0)]),e.isCsv()&&e.csvView==="table"?(l(),d("div",{key:0,class:"csv-table-wrap",tabindex:"0",role:"region","aria-label":"Table view of "+(e.paths?.[e.paths.length-1]||"file")},[n("table",cB,[n("thead",null,[n("tr",null,[t[81]||(t[81]=n("th",{class:"csv-rownum",scope:"col"},[n("span",{class:"sr-only"},"Row")],-1)),(l(!0),d(L,null,ie(e.csvRows()[0]||[],(a,u)=>(l(),d("th",{scope:"col",key:u,class:T({"is-num":e.csvNumCol(u)})},[(l(!0),d(L,null,ie(e.redactParts(a),(p,y)=>(l(),d(L,{key:y},[p.n?(l(),d("span",{key:0,class:"redact-slot",role:"img","aria-label":"Redacted term "+p.n},[n("i",null,c(p.n),1)],8,pB)):(l(),d(L,{key:1},[g(c(p.t),1)],64))],64))),128))],2))),128))])]),n("tbody",null,[(l(!0),d(L,null,ie(e.csvRows().slice(1),(a,u)=>(l(),d("tr",{key:u},[n("td",fB,c(u+1),1),(l(!0),d(L,null,ie(a,(p,y)=>(l(),d("td",{key:y,class:T({"is-num":e.csvNumCol(y)})},[(l(!0),d(L,null,ie(e.redactParts(p),(m,b)=>(l(),d(L,{key:b},[m.n?(l(),d("span",{key:0,class:"redact-slot",role:"img","aria-label":"Redacted term "+m.n},[n("i",null,c(m.n),1)],8,hB)):(l(),d(L,{key:1},[g(c(m.t),1)],64))],64))),128))],2))),128))]))),128))])])],8,uB)):f("v-if",!0),e.type!="html"&&!(e.isCsv()&&e.csvView==="table")?P((l(),d("div",mB,null,512)),[[r,{content:e.content,options:e.aceOption}]]):f("v-if",!0),e.type=="html"&&e.mdFindOpen?(l(),d("div",gB,[n("input",{type:"search",class:"md-find-input",placeholder:"Find on this page","aria-label":"Find on this page",value:e.mdFindQuery,onInput:t[26]||(t[26]=a=>{e.mdFindQuery=a.target.value,e.runMdFind()}),onKeydown:[t[27]||(t[27]=mt(ce(a=>a.shiftKey?e.stepMdFind(-1):e.stepMdFind(1),["prevent"]),["enter"])),t[28]||(t[28]=mt(ce(a=>e.toggleMdFind(!1),["prevent"]),["esc"]))]},null,40,vB),n("span",yB,[n("button",{type:"button","aria-label":"Previous match",onClick:t[29]||(t[29]=a=>e.stepMdFind(-1)),disabled:!e.mdFindTotal},[...t[82]||(t[82]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-left"})],-1)])],8,bB),n("button",{type:"button","aria-label":"Next match",onClick:t[30]||(t[30]=a=>e.stepMdFind(1)),disabled:!e.mdFindTotal},[...t[83]||(t[83]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-chevron-right"})],-1)])],8,wB)]),n("span",kB,c(e.mdFindQuery?e.mdFindTotal?e.mdFindIndex+1+" of "+e.mdFindTotal:"No results":""),1),n("button",{type:"button",class:T(["md-find-opt",{"is-on":e.mdFindCase}]),"aria-pressed":e.mdFindCase?"true":"false",title:"Match case","aria-label":"Match case",onClick:t[31]||(t[31]=a=>{e.mdFindCase=!e.mdFindCase,e.runMdFind()})},"Aa",10,CB),n("button",{type:"button",class:"md-find-close","aria-label":"Close find",onClick:t[32]||(t[32]=a=>e.toggleMdFind(!1))},"\xD7")])):f("v-if",!0),e.type=="html"?(l(),d("div",EB,[n("div",{class:"file-content markdown-body",innerHTML:e.renderReadme(e.sanitize(e.content))},null,8,SB),e.readmeToc().length>=3?(l(),d("aside",NB,[t[84]||(t[84]=n("div",{class:"readme-toc-head"},"On this page",-1)),n("nav",RB,[n("a",{href:"#",class:T(["lvl-2 toc-top",{"is-active":!e.activeHeading}]),onClick:t[33]||(t[33]=ce(a=>e.scrollToHeading(null),["prevent"]))},"Introduction",2),(l(!0),d(L,null,ie(e.readmeToc(),(a,u)=>(l(),d("a",{key:a.id,href:"#"+a.id,class:T(["lvl-"+a.level,{"is-active":e.activeHeading===a.id}]),onClick:ce(p=>e.scrollToHeading(a.id),["prevent"])},c(a.text),11,DB))),128))])])):f("v-if",!0)])):f("v-if",!0)],2)):f("v-if",!0),e.type=="html-doc"&&!e.showSource&&e.content!=null?(l(),sn(o,{key:2,content:e.content,"base-url":e.fileBaseUrl,"allow-scripts":e.allowScripts},null,8,["content","base-url","allow-scripts"])):f("v-if",!0),e.fileFrame()?(l(),d("section",{key:3,class:T(["file-card file-card-media","view-"+e.type])},[n("header",AB,[n("div",TB,[n("button",{type:"button",class:T(["btn btn-sm btn-icon-only btn-ghost card-files-btn",{"is-needed":e.sidebarHidden}]),title:"Show file tree",onClick:t[34]||(t[34]=a=>e.sidebarHidden?e.sidebarHidden=!1:e.sidebarCollapsed=!1)},[...t[85]||(t[85]=[n("svg",{class:"ui-icon","aria-hidden":"true"},[n("use",{href:"#ui-icon-sidebar"})],-1),n("span",{class:"card-files-label"},"Files",-1)])],2),n("ol",PB,[n("li",OB,[n("a",{href:e.safeUrl("/r/"+e.repoId+"/"),title:e.repoId,textContent:c(/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(e.repoId||"")?"repository":e.repoId)},null,8,IB)]),(l(!0),d(L,null,ie(e.paths,(a,u)=>(l(),d("li",{class:"breadcrumb-item","aria-current":u===e.paths.length-1?"page":null},[(l(!0),d(L,null,ie(e.redactParts(a),(p,y)=>(l(),d(L,{key:y},[p.n?(l(),d("span",{key:0,class:"redact-slot",role:"img","aria-label":"Redacted term "+p.n},[n("i",null,c(p.n),1)],8,MB)):(l(),d(L,{key:1},[g(c(p.t),1)],64))],64))),128))],8,_B))),256))]),n("span",VB,[e.type==
Line truncated
|
||||
`).length+1)),placeholder:`Jane Smith
|
||||
MIT CSAIL
|
||||
jane.smith@example.org`},null,8,RB),[[i,{state:e.viewState,set:r=>{e.terms=r},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),n("details",DB,[t[25]||(t[25]=n("summary",null,"Regex & replacement syntax",-1)),n("p",null,[t[21]||(t[21]=y("Matches become ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[22]||(t[22]=y(". Use ",-1)),t[23]||(t[23]=n("code",null,"term=>replacement",-1)),t[24]||(t[24]=y(" for a custom replacement. Regex is supported.",-1))])]),A(n("div",AB,"Terms are in an invalid format.",512),[[J,e.defaultsForm?.terms?.errors?.format]])])]),n("section",TB,[t[35]||(t[35]=n("h2",null,"Reviewer display",-1)),n("div",null,[n("div",PB,[n("label",_B,[A(n("input",OB,null,512),[[i,{state:e.viewState,set:r=>{e.options.link=r},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[27]||(t[27]=y("Keep links",-1))]),n("label",IB,[A(n("input",MB,null,512),[[i,{state:e.viewState,set:r=>{e.options.notebook=r},value:e.options?.notebook,form:"defaultsForm",options:{}}]]),t[28]||(t[28]=y("Render notebooks",-1))]),n("label",VB,[A(n("input",qB,null,512),[[i,{state:e.viewState,set:r=>{e.options.image=r},value:e.options?.image,form:"defaultsForm",options:{}}]]),t[29]||(t[29]=y("Show images",-1))]),n("label",$B,[A(n("input",LB,null,512),[[i,{state:e.viewState,set:r=>{e.options.pdf=r},value:e.options?.pdf,form:"defaultsForm",options:{}}]]),t[30]||(t[30]=y("Show PDFs",-1))]),n("label",UB,[A(n("input",FB,null,512),[[i,{state:e.viewState,set:r=>{e.options.loc=r},value:e.options?.loc,form:"defaultsForm",options:{}}]]),t[31]||(t[31]=y("Show line counts",-1))]),n("label",BB,[A(n("input",zB,null,512),[[i,{state:e.viewState,set:r=>{e.options.page=r},value:e.options?.page,form:"defaultsForm",options:{}}]]),t[32]||(t[32]=y("GitHub Pages",-1))])]),t[33]||(t[33]=n("p",{class:"settings-help"},"Images and PDFs are not anonymized.",-1)),t[34]||(t[34]=n("p",{class:"settings-help"},"GitHub Pages must use the anonymized branch.",-1))])]),n("section",HB,[t[38]||(t[38]=n("h2",null,"Source updates",-1)),n("div",null,[n("label",jB,[A(n("input",GB,null,512),[[i,{state:e.viewState,set:r=>{e.options.update=r},value:e.options?.update,form:"defaultsForm",options:{}}]]),t[36]||(t[36]=y("Auto-update from GitHub",-1))]),t[37]||(t[37]=n("p",{class:"settings-help"},"Latest commit, at most once an hour.",-1))])]),n("section",WB,[t[40]||(t[40]=n("label",{class:"settings-label",for:"expiration"},"On expiration",-1)),n("div",null,[A((l(),d("select",KB,[...t[39]||(t[39]=[n("option",{value:"redirect"},"Redirect visitors to the GitHub repository",-1),n("option",{value:"remove",selected:""},"Remove the anonymized repository",-1)])])),[[i,{state:e.viewState,set:r=>{e.options.expirationMode=r},value:e.options?.expirationMode,form:"defaultsForm",options:{}}]]),n("p",YB,c(e.options.expirationMode==="redirect"?"Redirecting reveals the original repository. Choose the date per anonymization.":"Set the expiration date for each repository."),1)])]),n("footer",XB,[n("div",JB,[e.draftRestored?(l(),d("span",QB,"Your unsaved draft was restored.")):e.unsavedChanges?(l(),d("span",ZB,"Unsaved changes"+c(e.draftSaved?" \xB7 Draft saved in this tab":""),1)):(l(),d("span",xB,c(e.message?"Defaults saved":"All changes saved"),1))]),e.draftRestored||e.unsavedChanges?(l(),d("button",{key:0,type:"button",class:"draft-discard",onClick:t[0]||(t[0]=r=>e.discardDraft())},"Discard changes")):f("v-if",!0),n("button",{id:"save",type:"submit",class:"btn btn-ink",disabled:e.saving},c(e.saving?"Saving\u2026":"Save defaults"),9,e7),e.error?(l(),d("div",t7,c(e.error),1)):f("v-if",!0)])],32)),[[s,e.viewState]])],512),[[J,e.settingsTab==="defaults"]]),A(n("section",{id:"settings-billing","aria-busy":e.billingLoading||e.billingBusy},[t[95]||(t[95]=n("p",{class:"settings-scope"},"Your plan and usage. Reviewer access is always free.",-1)),e.billingLoading?(l(),d("p",o7,"Loading billing\u2026")):f("v-if",!0),e.billingError?(l(),d("div",s7,[y(c(e.billingError)+" ",1),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[2]||(t[2]=r=>e.loadBilling()),disabled:e.billingBusy},"Try again",8,i7)])):f("v-if",!0),e.billingNotice?(l(),d("p",r7,c(e.billingNotice),1)):f("v-if",!0),e.billingPending?(l(),d("div",a7,[t[42]||(t[42]=n("strong",null,"Confirming your payment",-1)),t[43]||(t[43]=n("p",null,"Your allowance will update after confirmation. Please do not pay again.",-1)),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[3]||(t[3]=r=>e.refreshBilling()),disabled:e.billingBusy},"Check payment status",8,l7)])):f("v-if",!0),n("section",d7,[n("div",u7,[n("div",null,[t[44]||(t[44]=n("p",{class:"billing-eyebrow"},"Current plan",-1)),n("h2",c7,c(e.profileAuthorGrant()?e.profileGrantName(e.profileAuthorGrant()):e.billing?.enabled?"Free":"Account allowance"),1),e.profileAuthorGrant()?(l(),d("p",p7,[y(c(e.profileAuthorGrant().cancelAtPeriodEnd?"Renewal cancelled \xB7 access until ":"Renews ")+c(e.billingDate(e.profileAuthorGrant().until)),1),e.profi
Line truncated
|
||||
`);for(let g=0;g<h.length;g++){let m=h[g];if(m.startsWith("diff --git")){v(),p();continue}if(m.startsWith("--- ")){r&&r.lines.length&&v(),p().oldPath=m.replace(/^--- (a\/)?/,"").trim();continue}if(m.startsWith("+++ ")){p().newPath=m.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(m.startsWith("index ")||m.startsWith("similarity index")||m.startsWith("rename ")||m.startsWith("new file mode")||m.startsWith("deleted file mode")||m.startsWith("Binary files"))){if(m.startsWith("@@")){let k=m.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);k&&(a=parseInt(k[1],10),u=parseInt(k[2],10)),p().lines.push({kind:"hunk",oldNo:"",newNo:"",text:m});continue}r&&(m.startsWith("+")?(r.lines.push({kind:"add",oldNo:"",newNo:u,text:m.slice(1)}),u++):m.startsWith("-")?(r.lines.push({kind:"remove",oldNo:a,newNo:"",text:m.slice(1)}),a++):(r.lines.push({kind:"ctx",oldNo:a,newNo:u,text:m.startsWith(" ")?m.slice(1):m}),a++,u++))}}return o(s,r),s.join("")}})({trustAsHtml:e=>e});function h9(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function m9(e){return String(e??"").toUpperCase()}function g9(e,t,o=0){return(e||[]).slice(o,o+t)}function v9(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function y9(e,t){if(!t)return e||[];let o=Array.isArray(t)?t:[t];return[...e||[]].sort((i,s)=>{for(let r of o){let a=r[0]==="-"?-1:1;r=r.replace(/^[+-]/,"");let u=r.split(".").reduce((v,h)=>v?.[h],i),p=r.split(".").reduce((v,h)=>v?.[h],s);if(typeof u=="string"&&(u=u.toLowerCase()),typeof p=="string"&&(p=p.toLowerCase()),u!==p)return(u==null?-1:p==null?1:u<p?-1:1)*a}return 0})}function b9(e,t){if(!e)return"";let o=new Date(e);if(isNaN(o))return String(e);if(!t||t==="mediumDate")return o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let i={yyyy:o.getFullYear(),MMM:o.toLocaleDateString(void 0,{month:"short"}),MM:String(o.getMonth()+1).padStart(2,"0"),dd:String(o.getDate()).padStart(2,"0"),d:o.getDate(),HH:String(o.getHours()).padStart(2,"0"),mm:String(o.getMinutes()).padStart(2,"0"),ss:String(o.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,s=>i[s])}function w9(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var jc={name:"htmlDoc",props:["content","baseUrl","allowScripts"],setup(e){let t=gt(null);return on(()=>{let i=[t.value][0];i.classList.add("html-doc");function s(){i.innerHTML="";let r=e.content;if(typeof r!="string")return;let a=document.createElement("iframe");a.className="html-doc-frame",a.setAttribute("title","Rendered HTML document");let u=["allow-popups","allow-popups-to-escape-sandbox"];e.allowScripts&&u.push("allow-scripts","allow-forms","allow-modals"),a.setAttribute("sandbox",u.join(" ")),a.setAttribute("referrerpolicy","no-referrer"),i.appendChild(a);let p="";e.baseUrl&&(p='<base href="'+e.baseUrl.replace(/&/g,"&").replace(/"/g,""")+'">'),a.srcdoc=p+r}tt(()=>e.content,s,{immediate:!0}),tt(()=>e.baseUrl,s,{immediate:!0}),tt(()=>e.allowScripts,s,{immediate:!0})}),()=>_e("html-doc",{ref:t})}};var Gc={name:"pdfviewer",props:["src"],setup(e){let t=gt(null);return on(()=>{let o=[t.value],i=400,s=[.5,.75,1,1.25,1.5,2,3],r=1e3,a=o[0];a.classList.add("pdf-viewer");let u=null,p=[],v=!1,h=null,g=1,m=1,k=0,T=0,w=612/792,b=0,E=null,N=new Map,O=0;function q(C){let R=N.get(C);R&&(R.cancelled=!0,R.task?.cancel?.(),N.delete(C));let I=C.querySelector("canvas");I&&(I.width=0,I.height=0),C.innerHTML="",delete C.dataset.state}let M=document.createElement("div");M.className="pdf-toolbar";let G=document.createElement("div");G.className="pdf-pages",G.tabIndex=0,G.setAttribute("aria-label","PDF pages"),a.appendChild(M),a.appendChild(G);function de(C,R,I){let W=document.createElement("button");return W.type="button",W.className="pdf-toolbar-btn",W.title=R,W.setAttribute("aria-label",R),W.innerHTML='<i class="fas '+C+'"></i>',W.addEventListener("click",I),W}let re=de("fa-chevron-up","Previous page",function(){Ke(m-1)}),D=de("fa-chevron-down","Next page",function(){Ke(m+1)}),X=document.createElement("input");X.type="text",X.className="pdf-toolbar-page",X.setAttribute("aria-label","Page number"),X.title="Page number \u2014 type a page and press Enter",X.addEventListener("keydown",function(C){C.key==="Enter"&&(Ke(parseInt(X.value,10)),X.blur())}),X.addEventListener("blur",function(){X.value=String(m)});let le=document.createElement("span");le.className="pdf-toolbar-count";let Ee=de("fa-search-minus","Zoom out",function(){ue(s[Math.max(0,Ne()-1)])}),ae=de("fa-search-plus","Zoom in",function(){ue(s[Math.min(s.length-1,Ne()+1)])}),he=document.createElement("button");he.type="button",he.className="pdf-toolbar-btn pdf-toolbar-zoom",he.title="Reset zoom to fit the width",he.setAttribute("aria-label","Reset zoom to fit the width"),he.addEventListener("click",function(){ue(1)}),M.appendChild(re),M.appendChild(D);let Ce=document.createElement("span");Ce.className="pdf-toolbar-group",Ce.append
Line truncated
|
||||
jane.smith@example.org`},null,8,jH),[[i,{state:e.viewState,set:r=>{e.terms=r},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),n("details",GH,[t[25]||(t[25]=n("summary",null,"Regex & replacement syntax",-1)),n("p",null,[t[21]||(t[21]=g("Matches become ",-1)),n("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[22]||(t[22]=g(". Use ",-1)),t[23]||(t[23]=n("code",null,"term=>replacement",-1)),t[24]||(t[24]=g(" for a custom replacement. Regex is supported.",-1))])]),P(n("div",WH,"Terms are in an invalid format.",512),[[ee,e.defaultsForm?.terms?.errors?.format]])])]),n("section",KH,[t[35]||(t[35]=n("h2",null,"Reviewer display",-1)),n("div",null,[n("div",YH,[n("label",XH,[P(n("input",JH,null,512),[[i,{state:e.viewState,set:r=>{e.options.link=r},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[27]||(t[27]=g("Keep links",-1))]),n("label",QH,[P(n("input",ZH,null,512),[[i,{state:e.viewState,set:r=>{e.options.image=r},value:e.options?.image,form:"defaultsForm",options:{}}]]),t[28]||(t[28]=g("Display images",-1))]),n("label",xH,[P(n("input",ej,null,512),[[i,{state:e.viewState,set:r=>{e.options.pdf=r},value:e.options?.pdf,form:"defaultsForm",options:{}}]]),t[29]||(t[29]=g("Display PDFs",-1))]),n("label",tj,[P(n("input",nj,null,512),[[i,{state:e.viewState,set:r=>{e.options.notebook=r},value:e.options?.notebook,form:"defaultsForm",options:{}}]]),t[30]||(t[30]=g("Display notebooks",-1))]),n("label",oj,[P(n("input",sj,null,512),[[i,{state:e.viewState,set:r=>{e.options.loc=r},value:e.options?.loc,form:"defaultsForm",options:{}}]]),t[31]||(t[31]=g("Show line counts",-1))]),n("label",ij,[P(n("input",rj,null,512),[[i,{state:e.viewState,set:r=>{e.options.page=r},value:e.options?.page,form:"defaultsForm",options:{}}]]),t[32]||(t[32]=g("GitHub Pages",-1))])]),t[33]||(t[33]=n("p",{class:"settings-help"},"Images and PDFs are not anonymized.",-1)),t[34]||(t[34]=n("p",{class:"settings-help"},"GitHub Pages must use the anonymized branch.",-1))])]),n("section",aj,[t[38]||(t[38]=n("h2",null,"Source updates",-1)),n("div",null,[n("label",lj,[P(n("input",dj,null,512),[[i,{state:e.viewState,set:r=>{e.options.update=r},value:e.options?.update,form:"defaultsForm",options:{}}]]),t[36]||(t[36]=g("Auto-update from GitHub",-1))]),t[37]||(t[37]=n("p",{class:"settings-help"},"Latest commit, at most once an hour.",-1))])]),n("section",uj,[t[40]||(t[40]=n("label",{class:"settings-label",for:"expiration"},"On expiration",-1)),n("div",null,[P((l(),d("select",cj,[...t[39]||(t[39]=[n("option",{value:"redirect"},"Redirect visitors to the GitHub repository",-1),n("option",{value:"remove",selected:""},"Remove the anonymized repository",-1)])])),[[i,{state:e.viewState,set:r=>{e.options.expirationMode=r},value:e.options?.expirationMode,form:"defaultsForm",options:{}}]]),n("p",pj,c(e.options.expirationMode==="redirect"?"Redirecting reveals the original repository. Choose the date per anonymization.":"Set the expiration date for each repository."),1)])]),n("footer",fj,[n("div",hj,[e.draftRestored?(l(),d("span",mj,"Your unsaved draft was restored.")):e.unsavedChanges?(l(),d("span",gj,"Unsaved changes"+c(e.draftSaved?" \xB7 Draft saved in this tab":""),1)):(l(),d("span",vj,c(e.message?"Defaults saved":"No unsaved changes"),1))]),e.draftRestored||e.unsavedChanges?(l(),d("button",{key:0,type:"button",class:"draft-discard",onClick:t[0]||(t[0]=r=>e.discardDraft())},"Discard changes")):f("v-if",!0),n("button",{id:"save",type:"submit",class:T(["btn btn-ink",{"is-saving":e.saving}]),disabled:e.saving||!(e.unsavedChanges||e.draftRestored)},c(e.saving?"Saving\u2026":"Save defaults"),11,yj),e.error?(l(),d("div",bj,c(e.error),1)):f("v-if",!0)])],32)),[[s,e.viewState]])],512),[[ee,e.settingsTab==="defaults"]]),P(n("section",{id:"settings-billing","aria-busy":e.billingLoading||e.billingBusy},[t[95]||(t[95]=n("p",{class:"settings-scope"},"Your plan and usage. Reviewer access is always free.",-1)),e.billingLoading?(l(),d("p",kj,"Loading billing\u2026")):f("v-if",!0),e.billingError?(l(),d("div",Cj,[g(c(e.billingError)+" ",1),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[3]||(t[3]=r=>e.loadBilling()),disabled:e.billingBusy},"Try again",8,Ej)])):f("v-if",!0),e.billingNotice?(l(),d("p",Sj,c(e.billingNotice),1)):f("v-if",!0),e.billingPending?(l(),d("div",Nj,[t[42]||(t[42]=n("strong",null,"Confirming your payment",-1)),t[43]||(t[43]=n("p",null,"Your allowance will update after confirmation. Please do not pay again.",-1)),n("button",{type:"button",class:"btn btn-outline-ink",onClick:t[4]||(t[4]=r=>e.refreshBilling()),disabled:e.billingBusy},"Check payment status",8,Rj)])):f("v-if",!0),n("section",Dj,[n("div",Aj,[n("div",null,[t[44]||(t[44]=n("p",{class:"billing-eyebrow"},"Current plan",-1)),n("h2",Tj,c(e.profileAuthorGrant()?e.profileGrantName(e.profileAuthorGrant()):e.billing?.enabled?"Free":"Account allowance"),1),e.profileAuthorGrant()?(l(),d("p",Pj,[g(c(e.profileAuthorGrant().cancelAtPeriodEnd?"Renewal cancelled \xB7 access
Line truncated
|
||||
/*! Bundled license information:
|
||||
|
||||
@vue/shared/dist/shared.esm-bundler.js:
|
||||
|
||||
+146
-4
@@ -71,6 +71,16 @@ const repositories = [
|
||||
source: { fullName: "tdurieux/anonymous_github", branch: "main", commit: "f817a29a0000" },
|
||||
options: opts({ update: true }),
|
||||
},
|
||||
{
|
||||
// Removed link with a single view: shows the singular unit in the Removed tab.
|
||||
repoId: "icse24-artifact-7B1D",
|
||||
status: "removed",
|
||||
anonymizeDate: ago(700),
|
||||
lastView: ago(640),
|
||||
pageView: 1,
|
||||
source: { fullName: "tdurieux/icse24-artifact", branch: "main", commit: "9e1b44c20000" },
|
||||
options: opts(),
|
||||
},
|
||||
{
|
||||
repoId: "JarSift",
|
||||
status: "expired",
|
||||
@@ -189,7 +199,7 @@ const conferences = [
|
||||
endDate: ahead(30),
|
||||
status: "ready",
|
||||
options: opts({ page: true }),
|
||||
plan: { planID: "premium_conference", name: "Premium", pricePerRepository: 0.5 },
|
||||
plan: { planID: "conference", name: "Conference", pricePerRepository: 0 },
|
||||
price: 12.5,
|
||||
nbRepositories: 2,
|
||||
repositories: repositories.slice(0, 4).map((r) => Object.assign({}, r, { addDate: ago(60) })),
|
||||
@@ -249,11 +259,12 @@ const history = Array.from({ length: 60 }, (_, i) => ({
|
||||
}));
|
||||
|
||||
// ANON=1 serves the signed-out experience (landing page, FAQ) instead;
|
||||
// ADMIN=1 signs in as an administrator so /admin pages render.
|
||||
// ADMIN=1 (or a `mock_admin=1` cookie, for one browser context only) signs in
|
||||
// as an administrator so /admin pages render.
|
||||
app.get("/api/user", (req, res) =>
|
||||
process.env.ANON
|
||||
? res.status(401).json({ error: "not_connected" })
|
||||
: res.json({ username: "tdurieux", photo: "https://avatars.githubusercontent.com/u/5577568?v=4", isAdmin: !!process.env.ADMIN })
|
||||
: res.json({ username: "tdurieux", photo: "https://avatars.githubusercontent.com/u/5577568?v=4", isAdmin: !!process.env.ADMIN || /(?:^|;\s*)mock_admin=1/.test(req.headers.cookie || "") })
|
||||
);
|
||||
// Local connection fixtures keep the account screens usable without GitHub.
|
||||
app.get("/github/connections", (req, res) => res.json({
|
||||
@@ -270,7 +281,23 @@ app.get("/api/user/default", (req, res) =>
|
||||
res.json({ terms: ["Jane Smith", "MIT CSAIL"], options: { link: true, image: true, pdf: true, notebook: true, loc: true, page: false, update: false, mode: "GitHubStream", expirationMode: "remove" } })
|
||||
);
|
||||
app.post("/api/user/default", (req, res) => res.json({}));
|
||||
app.get("/api/conferences/plans", (req, res) => res.json(plans));
|
||||
// Conference review packages (billing enabled), mirroring src/core/billing/config.ts.
|
||||
// Served only with a `mock_billing=1` cookie so the other screens keep the
|
||||
// billing-disabled experience.
|
||||
const GiB = 1024 ** 3;
|
||||
const conferenceTiers = [
|
||||
{ id: "conference_workshop", name: "Workshop", repositories: 25, storage: 10 * GiB, amount: 14900 },
|
||||
{ id: "conference", name: "Conference", repositories: 100, storage: 50 * GiB, amount: 49000 },
|
||||
{ id: "conference_large", name: "Large conference", repositories: 400, storage: 200 * GiB, amount: 149000 },
|
||||
];
|
||||
const billingOn = (req) => /(?:^|;\s*)mock_billing=1/.test(req.headers.cookie || "");
|
||||
app.get("/api/conferences/plans", (req, res) => res.json(billingOn(req)
|
||||
? conferenceTiers.map((t) => ({ id: t.id, name: t.name, pricePerRepo: 0, storagePerRepo: t.storage, repositories: t.repositories, description: `<li>Up to ${t.repositories} repositories</li><li>Up to 180 days</li><li>Prepaid at checkout</li>` }))
|
||||
: plans));
|
||||
app.get("/api/billing/", (req, res) => billingOn(req)
|
||||
? res.json({ enabled: true, csrf: "local-preview-only", checkoutEnabled: true, freeExpirationMonths: 12, grants: [], purchases: [], repositories: [], portal: false, allowance: null,
|
||||
plans: conferenceTiers.map((t) => ({ id: t.id, name: t.name, description: "", repositories: t.repositories, storage: t.storage, amount: t.amount, currency: "eur" })) })
|
||||
: res.status(404).json({ error: "not mocked: /api/billing/" }));
|
||||
app.get("/api/conferences/", (req, res) => res.json(conferences));
|
||||
app.get("/api/conferences/:id", (req, res) => {
|
||||
const c = conferences.find((x) => x.conferenceID === req.params.id);
|
||||
@@ -329,6 +356,75 @@ app.get(`/api/repo/${EXPLORER_ID}/file/{*rest}`, (req, res) => {
|
||||
res.type("text/plain").send(explorerContent[file]);
|
||||
});
|
||||
|
||||
// Viewer gallery: a second explorer fixture with one file of every kind the
|
||||
// reviewer view renders (PDF, image, SVG, video, notebook, CSV, binary, empty
|
||||
// file, empty folder, and a file over the size limit). Binaries live in
|
||||
// scripts/fixtures/explorer-gallery; content types follow the real file route.
|
||||
const GALLERY_ID = "replication-package-5E1A";
|
||||
const GALLERY_DIR = path.join(__dirname, "fixtures", "explorer-gallery");
|
||||
const galleryPng = fs.readFileSync(path.join(GALLERY_DIR, "figures", "accuracy.png"));
|
||||
const galleryNotebook = JSON.stringify({
|
||||
nbformat: 4, nbformat_minor: 5,
|
||||
metadata: { kernelspec: { name: "python3", display_name: "Python 3" }, language_info: { name: "python" } },
|
||||
cells: [
|
||||
{ cell_type: "markdown", metadata: {}, source: ["# Results analysis\n", "\n", "Reproduces Table 2 and Figure 1 of the paper. Run the cells in order; the data is loaded from `results.csv`."] },
|
||||
{ cell_type: "code", execution_count: 1, metadata: {}, outputs: [], source: ["import pandas as pd\n", "df = pd.read_csv(\"results.csv\")\n", "df.groupby(\"model\").accuracy.mean()"] },
|
||||
{ cell_type: "code", execution_count: 2, metadata: {}, source: ["print(f\"{len(df)} runs by XXXX-1\")"], outputs: [{ output_type: "stream", name: "stdout", text: ["12 runs by XXXX-1\n"] }] },
|
||||
{ cell_type: "code", execution_count: 3, metadata: {}, source: ["df.pivot_table(index=\"model\", values=[\"accuracy\", \"plausible\"]).round(1)"], outputs: [{ output_type: "execute_result", execution_count: 3, metadata: {}, data: {
|
||||
"text/html": ["<table border=\"1\" class=\"dataframe\"><thead><tr><th>model</th><th>accuracy</th><th>plausible</th></tr></thead><tbody><tr><th>Baseline</th><td>71.2</td><td>412.0</td></tr><tr><th>+ Aug.</th><td>74.0</td><td>447.0</td></tr><tr><th>Ours</th><td>78.9</td><td>503.0</td></tr></tbody></table>"],
|
||||
"text/plain": [" accuracy plausible\nBaseline 71.2 412.0\nOurs 78.9 503.0"] } }] },
|
||||
{ cell_type: "markdown", metadata: {}, source: ["## Figure 1\n", "\n", "Accuracy of each configuration on the held-out split."] },
|
||||
{ cell_type: "code", execution_count: 4, metadata: {}, source: ["ax = df.groupby(\"model\").accuracy.mean().plot.bar()\n", "ax.figure.savefig(\"figures/accuracy.png\")"], outputs: [{ output_type: "display_data", metadata: {}, data: { "image/png": galleryPng.toString("base64"), "text/plain": ["<Figure size 720x400 with 1 Axes>"] } }] },
|
||||
],
|
||||
}, null, 1);
|
||||
const galleryText = {
|
||||
"README.md": "# Replication package\n\nArtifact for *Learning Robust Repair Strategies from Developer Histories*, prepared by XXXX-1.\n\n| File | What it shows |\n| --- | --- |\n| `paper.pdf` | The submitted paper |\n| `analysis.ipynb` | Notebook that rebuilds Table 2 and Figure 1 |\n| `figures/` | Figures as PNG and SVG |\n| `demo/` | A short screen recording of a full run |\n\n\n",
|
||||
"results.csv": "model,seed,accuracy,plausible\nBaseline,1,71.0,410\nBaseline,2,71.4,414\n+ Aug.,1,73.8,445\n+ Aug.,2,74.2,449\n+ Pretrain,1,76.0,470\n+ Pretrain,2,76.2,472\nOurs,1,78.7,501\nOurs,2,79.1,505\n",
|
||||
"figures/pipeline.svg": '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="200" viewBox="0 0 640 200" font-family="Inter, Arial, sans-serif" font-size="14"><rect width="640" height="200" fill="#ffffff"/><g fill="#eef6f4" stroke="#1f6f66" stroke-width="1.5"><rect x="20" y="70" width="150" height="60" rx="8"/><rect x="245" y="70" width="150" height="60" rx="8"/><rect x="470" y="70" width="150" height="60" rx="8"/></g><g fill="#1b1a18" text-anchor="middle"><text x="95" y="105">Mine histories</text><text x="320" y="105">Pretrain on edits</text><text x="545" y="105">Rank patches</text></g><g stroke="#6f6a61" stroke-width="1.5" marker-end="url(#a)"><line x1="172" y1="100" x2="240" y2="100"/><line x1="397" y1="100" x2="465" y2="100"/></g><defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#6f6a61"/></marker></defs></svg>\n',
|
||||
"analysis.ipynb": galleryNotebook,
|
||||
"logs/.gitkeep": "",
|
||||
};
|
||||
const galleryBinary = { "paper.pdf": "paper.pdf", "figures/accuracy.png": "figures/accuracy.png", "demo/demo.mp4": "demo/demo.mp4", "demo/demo.webm": "demo/demo.webm" };
|
||||
// Deterministic bytes standing in for a model checkpoint.
|
||||
const galleryCheckpoint = Buffer.from(Array.from({ length: 4096 }, (_, i) => (i * 131 + 7) % 256));
|
||||
const gallerySize = (name) =>
|
||||
galleryBinary[name] ? fs.statSync(path.join(GALLERY_DIR, galleryBinary[name])).size
|
||||
: name === "models/checkpoint.bin" ? galleryCheckpoint.length
|
||||
: name === "data/train.parquet" ? 312 * 1024 * 1024
|
||||
: Buffer.byteLength(galleryText[name] || "");
|
||||
const galleryTree = {
|
||||
"": ["data", "demo", "figures", "logs", "models", "outputs", "README.md", "analysis.ipynb", "paper.pdf", "results.csv"],
|
||||
data: ["train.parquet"],
|
||||
demo: ["demo.mp4", "demo.webm"],
|
||||
figures: ["accuracy.png", "pipeline.svg"],
|
||||
logs: [".gitkeep"],
|
||||
models: ["checkpoint.bin"],
|
||||
outputs: [],
|
||||
};
|
||||
const galleryFiles = (dir) => (galleryTree[dir] || []).map((name, i) => {
|
||||
const full = (dir ? dir + "/" : "") + name;
|
||||
return full in galleryTree ? { name, path: dir } : { name, path: dir, size: gallerySize(full), sha: "g" + dir.length + i };
|
||||
});
|
||||
app.get(`/api/repo/${GALLERY_ID}/options`, (req, res) =>
|
||||
res.json({ url: null, download: true, lastUpdateDate: ago(5), anonymizedAt: ago(5), sourceCommitDate: ago(6), expirationDate: ahead(90), isAdmin: false, isOwner: false, hasWebsite: false, truncatedFolders: [], hasSubmodules: false })
|
||||
);
|
||||
app.get(`/api/repo/${GALLERY_ID}/files/counts`, (req, res) =>
|
||||
res.json({ "": 13, data: 1, demo: 2, figures: 2, logs: 1, models: 1, outputs: 0 })
|
||||
);
|
||||
app.get(`/api/repo/${GALLERY_ID}/files`, (req, res) => res.json(galleryFiles(req.query.path || "")));
|
||||
app.get(`/api/repo/${GALLERY_ID}/file/{*rest}`, (req, res) => {
|
||||
const file = req.params.rest.join("/");
|
||||
if (req.query.download) res.attachment(path.basename(file));
|
||||
if (file === "data/train.parquet") return res.status(413).json({ error: "file_too_big" });
|
||||
if (file === "models/checkpoint.bin") return res.type("application/octet-stream").send(galleryCheckpoint);
|
||||
if (galleryBinary[file]) return res.sendFile(path.join(GALLERY_DIR, galleryBinary[file]));
|
||||
if (file in galleryText) {
|
||||
const mime = require("mime-types").lookup(file);
|
||||
return res.type(mime && !file.endsWith(".ts") ? mime : "text/plain").send(galleryText[file]);
|
||||
}
|
||||
res.status(404).json({ error: "file_not_found" });
|
||||
});
|
||||
|
||||
app.get("/api/repo/:id", (req, res) => {
|
||||
const r = repositories.find((x) => x.repoId === req.params.id) || repositories[0];
|
||||
res.json(r);
|
||||
@@ -448,6 +544,12 @@ const statusCounts = (rows, withStorage) => {
|
||||
const page = (req, rows, withStorage) => {
|
||||
const counts = statusCounts(rows, withStorage);
|
||||
const sort = { [req.query.sort || "_id"]: req.query.direction === "asc" ? 1 : -1 };
|
||||
// Status flags filter like src/server/routes/admin.ts; counts stay global.
|
||||
const groups = { ready: ["ready"], error: ["error"], expired: ["expired", "expiring"], removed: ["removed", "removing"], preparing: ["preparing", "download"] };
|
||||
if (Object.keys(groups).some((k) => req.query[k] != null)) {
|
||||
const allowed = Object.keys(groups).filter((k) => req.query[k] == "true").flatMap((k) => groups[k]);
|
||||
if (allowed.length) rows = rows.filter((r) => allowed.includes(r.status));
|
||||
}
|
||||
const out = { query: {}, page: parseInt(req.query.page, 10) || 1, total: rows.length, sort, results: rows, statusCounts: counts };
|
||||
if (withStorage) out.totalSize = counts.reduce((t, c) => t + (c.storage || 0), 0);
|
||||
return out;
|
||||
@@ -694,6 +796,40 @@ const adminTokens = [
|
||||
const ok = (req, res) => res.json({ ok: true });
|
||||
app.get("/api/admin/overview", (req, res) => res.json(overview));
|
||||
app.get("/api/admin/performance", (req, res) => res.json(performanceFor(Number(req.query.minutes))));
|
||||
// Shape of GET /api/admin/cache (cacheMetricsReport + blob-cache stats()).
|
||||
const CACHE_BUCKETS = [0.1, 1, 10, 100, 1000, 10000];
|
||||
const cacheRow = (name, traffic, count, bytes, msEach, bucket) => {
|
||||
const row = { name, traffic, count, bytes, ms: msEach * count };
|
||||
for (let i = 0; i <= CACHE_BUCKETS.length; i++) row["b" + i] = i === bucket ? Math.ceil(count * 0.9) : i === bucket + 1 ? Math.floor(count * 0.1) : 0;
|
||||
return row;
|
||||
};
|
||||
const cacheFor = (minutes) => {
|
||||
const k = ({ 15: 0.25, 60: 1, 1440: 18, 10080: 110 })[minutes] || 1;
|
||||
const n = (v) => Math.max(1, Math.round(v * k));
|
||||
return {
|
||||
available: true, windowMinutes: [15, 60, 1440, 10080].includes(minutes) ? minutes : 60, sampledAt: Date.now() - 6000, lastFlush: Date.now() - 6000, dropped: 0,
|
||||
enabled: true, filesystemSupported: true, latencyBucketsMs: CACHE_BUCKETS,
|
||||
source: { blobs: 18240, originalBytes: 61.2 * 1024 * MB, storedBytes: 19.4 * 1024 * MB, allocatedBytes: 21.7 * 1024 * MB, activeReaders: 14, decisions: 0, reservedBytes: 96 * MB, budgetBytes: 50 * 1024 * MB, maxObjectBytes: 32 * MB, oldestAccess: Date.now() - 21 * DAY },
|
||||
results: { blobs: 5210, originalBytes: 3.1 * 1024 * MB, storedBytes: 0.9 * 1024 * MB, allocatedBytes: 1.1 * 1024 * MB, activeReaders: 3, decisions: 40211, reservedBytes: 0, budgetBytes: 512 * MB, maxObjectBytes: 32 * MB, oldestAccess: Date.now() - 9 * DAY },
|
||||
rows: [
|
||||
cacheRow("compress", "file", n(210), n(210) * 48000, 7.8, 2),
|
||||
cacheRow("decision_hit", "file", n(8120), n(8120) * 62000, 0, 0),
|
||||
cacheRow("decision_miss", "file", n(640), n(640) * 54000, 0, 0),
|
||||
cacheRow("decompress", "file", n(7930), n(7930) * 61000, 0.31, 1),
|
||||
cacheRow("inspected_changed", "file", n(410), n(410) * 22000, 3.4, 2),
|
||||
cacheRow("inspected_unchanged", "file", n(230), n(230) * 18000, 1.1, 2),
|
||||
cacheRow("rewrite_changed", "file", n(410), n(410) * 22000, 47.9, 3),
|
||||
cacheRow("source_hit", "file", n(7710), n(7710) * 64000, 0, 0),
|
||||
cacheRow("source_miss", "file", n(1050), n(1050) * 71000, 0, 0),
|
||||
cacheRow("source_hit", "website", n(1320), n(1320) * 12000, 0, 0),
|
||||
cacheRow("source_miss", "website", n(88), n(88) * 15000, 0, 0),
|
||||
cacheRow("source_hit", "zip", n(36), n(36) * 4.1 * MB, 0, 0),
|
||||
cacheRow("source_bypass", "monitor", n(540), 0, 0, 0),
|
||||
cacheRow("result_fill", "background", n(120), n(120) * 21000, 12.5, 2),
|
||||
],
|
||||
};
|
||||
};
|
||||
app.get("/api/admin/cache", (req, res) => res.json(cacheFor(Number(req.query.minutes))));
|
||||
app.get("/api/admin/stats", (req, res) => res.json({
|
||||
statusBreakdown: overview.repos.statusBreakdown, totalStorage: overview.repos.totalStorage,
|
||||
recentErrors24h: overview.repos.recentErrors24h, totalUsers: overview.users.total, totalConferences: adminConferences.length,
|
||||
@@ -777,6 +913,12 @@ app.use((req, res, next) => {
|
||||
});
|
||||
// index: false so "/" falls through to the placeholder-filled index.html below.
|
||||
app.use(express.static(PUBLIC_DIR, { etag: false, cacheControl: false, index: false }));
|
||||
// Like src/server/index.ts: a README's relative image (/r/<id>/figures/x.png,
|
||||
// requested without text/html) redirects to the file endpoint.
|
||||
app.get("/r/:repoId/{*rest}", (req, res, next) => {
|
||||
if ((req.headers.accept || "").includes("text/html")) return next();
|
||||
res.redirect(`/api/repo/${req.params.repoId}/file/${req.params.rest.join("/")}`);
|
||||
});
|
||||
app.get("/{*rest}", (req, res) => res.type("html").send(indexHtml()));
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
@@ -44,6 +44,6 @@ describe("dashboard .cell-conf overflow fix", function () {
|
||||
|
||||
it("exposes the full conference value via a title attribute for truncated text", function () {
|
||||
const html = fs.readFileSync(htmlPath, "utf8");
|
||||
expect(html).to.match(/cell-conf[\s\S]*?:title="\(item\?\.conference\)"/);
|
||||
expect(html).to.match(/cell-conf[\s\S]*?:title="[^"]*item\??\.conference\b/);
|
||||
});
|
||||
});
|
||||
+29
-23
@@ -248,7 +248,7 @@ describe("Vue 3 UI", function () {
|
||||
expect(ui.window.document.querySelector(".settings-comparison").textContent).to.include("Automatic updates");
|
||||
expect(ui.window.document.querySelectorAll(".settings-nav button")[1].getAttribute("aria-current")).to.equal("page");
|
||||
expect(ui.window.document.querySelector("#settings-billing").textContent).to.include("/ month");
|
||||
expect(ui.window.document.querySelector("#settings-billing").textContent).to.include("10 GiB");
|
||||
expect(ui.window.document.querySelector("#settings-billing").textContent).to.include("10 GB");
|
||||
[...ui.window.document.querySelectorAll(".billing-offer .btn")].find(b => b.textContent.trim() === "Subscribe").click();
|
||||
await delay(30);
|
||||
const request = ui.requests.find(r => r.url.pathname === "/api/billing/checkout");
|
||||
@@ -547,9 +547,9 @@ describe("Vue 3 UI", function () {
|
||||
state.options.expirationMode = "never"; await delay(20);
|
||||
expect(doc.querySelector('.hosting-options').textContent).to.include("20 repositories");
|
||||
expect(doc.querySelector('.anonymize-submit-bar').textContent).to.include("Continue to checkout");
|
||||
doc.querySelector('.preview-expand').click(); await delay(10);
|
||||
doc.querySelector('.reviewer-preview .rp-open').click(); await delay(10);
|
||||
expect(doc.querySelector('.anonymize-page').classList.contains('preview-expanded')).to.equal(true);
|
||||
doc.querySelector('.preview-expand').click(); await delay(10);
|
||||
doc.querySelector('.reviewer-preview .rp-open').click(); await delay(10);
|
||||
expect(state.terms).to.equal("Private Author");
|
||||
expect(ui.requests.some(r => r.method === "POST" && r.url.pathname === "/api/repo/")).to.equal(false);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
@@ -854,6 +854,8 @@ describe("Vue 3 UI", function () {
|
||||
|
||||
it("uses a readable fallback for unknown settings errors", async () => {
|
||||
ui = await browser("/profile", { ...repositorySource, "/api/user/default": request => request.method === "POST" ? { __status: 503, body: { error: "untranslated_failure" } } : { terms: [], options: {} } });
|
||||
// Save is disabled on a clean form, so make a change first.
|
||||
await ui.input("#terms", "Changed Author");
|
||||
ui.window.document.querySelector("#save").click(); await delay(30);
|
||||
expect(ui.window.document.querySelector('[role="alert"]').textContent).to.include("Unable to save your defaults");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
@@ -1164,7 +1166,7 @@ describe("Vue 3 UI", function () {
|
||||
{ name: "source_hit", traffic: "monitor", count: 10, bytes: 1000, ms: 0 }] } });
|
||||
await ui.go("/admin/");
|
||||
const panel = ui.window.document.querySelector(".ov-cache");
|
||||
expect(panel.textContent).to.include("4.0 KB / 64.0 KB");
|
||||
expect(panel.textContent).to.include("4 KB / 64 KB");
|
||||
await ui.input("#cache-traffic", "file", "change");
|
||||
expect(panel.textContent).to.include("80.0%");
|
||||
await ui.input("#cache-traffic", "zip", "change");
|
||||
@@ -1200,18 +1202,14 @@ describe("Vue 3 UI", function () {
|
||||
runtimeSeries: [{ instance: sample.instance, rss: 20 * 1024 * 1024 }] } });
|
||||
await ui.go("/admin/");
|
||||
const text = ui.window.document.querySelector(".app-view").textContent;
|
||||
expect(text).to.include("Request performance"); expect(text).to.include("\u2264 250 ms");
|
||||
expect(text).to.include("\u2264 50 ms"); expect(text).to.include("streamer:fixture"); expect(text).to.include("20.0 MB");
|
||||
const routesTab = ui.window.document.querySelector("#performance-routes-tab");
|
||||
routesTab.dispatchEvent(new ui.window.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
|
||||
await delay(10);
|
||||
expect(ui.window.document.activeElement.id).to.equal("performance-processes-tab");
|
||||
expect(ui.window.document.activeElement.getAttribute("aria-selected")).to.equal("true");
|
||||
expect(text).to.include("Request performance"); expect(text).to.include("\u2264 250\u202Fms");
|
||||
expect(text).to.include("\u2264 50\u202Fms"); expect(text).to.include("streamer:fixture"); expect(text).to.include("20 MB");
|
||||
await ui.input('select[aria-label="Performance metrics"]', "processes", "change");
|
||||
expect(ui.window.document.querySelector("#performance-processes").style.display).not.to.equal("none");
|
||||
expect(ui.window.document.querySelector("#performance-routes").style.display).to.equal("none");
|
||||
expect(ui.window.document.querySelector(".ov-metric-warning-row").textContent).to.include("Needs attention");
|
||||
ui.window.document.activeElement.dispatchEvent(new ui.window.KeyboardEvent("keydown", { key: "End", bubbles: true }));
|
||||
await delay(10);
|
||||
expect(ui.window.document.activeElement.id).to.equal("performance-stages-tab");
|
||||
await ui.input('select[aria-label="Performance metrics"]', "stages", "change");
|
||||
expect(ui.window.document.querySelector("#performance-stages").style.display).not.to.equal("none");
|
||||
const stageRows = ui.window.document.querySelectorAll("#performance-stages tbody tr");
|
||||
expect(stageRows).to.have.length(1);
|
||||
expect(stageRows[0].textContent).to.include("cache_hit");
|
||||
@@ -1259,8 +1257,7 @@ describe("Vue 3 UI", function () {
|
||||
expect(calls).to.equal(2);
|
||||
expect(ui.window.document.querySelector(".ov-performance [role=alert]")).to.equal(null);
|
||||
expect(ui.window.document.querySelector("#performance-routes").textContent).to.include("No recorded requests");
|
||||
ui.window.document.querySelector("#performance-stages-tab").click();
|
||||
await delay(10);
|
||||
await ui.input('select[aria-label="Performance metrics"]', "stages", "change");
|
||||
expect(ui.window.document.querySelector("#performance-stages").textContent).to.include("No recorded work stages");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
@@ -1429,7 +1426,7 @@ describe("Vue 3 UI", function () {
|
||||
input.dispatchEvent(new ui.window.Event("blur"));
|
||||
await delay(60);
|
||||
const workspace = ui.window.document.querySelector(".anonymize-workspace");
|
||||
const button = [...workspace.querySelectorAll("button")].find(b => b.textContent.trim() === "Manage access");
|
||||
const button = [...workspace.querySelectorAll("button")].find(b => b.textContent.trim() === "Manage GitHub access");
|
||||
expect(button).not.to.equal(undefined);
|
||||
for (let node = button; node; node = node.parentElement) expect(ui.window.getComputedStyle(node).display).not.to.equal("none");
|
||||
expect(workspace.textContent).to.include("Signing in does not grant access to private repositories");
|
||||
@@ -1513,7 +1510,7 @@ describe("Vue 3 UI", function () {
|
||||
"/api/repo/owner/repo/branches": [{ name: "main", commit: "abcdef123" }],
|
||||
"/api/repo/owner/repo/readme": "",
|
||||
});
|
||||
const button = [...ui.window.document.querySelectorAll("button")].find(b => b.textContent.includes(type === "repo" ? "Save changes" : "Update "));
|
||||
const button = [...ui.window.document.querySelectorAll("button")].find(b => b.textContent.includes("Save changes"));
|
||||
const posts = () => ui.requests.filter(r => r.method === "POST" && r.url.pathname === endpoint);
|
||||
button.click();
|
||||
await delay(10);
|
||||
@@ -1602,7 +1599,7 @@ describe("Vue 3 UI", function () {
|
||||
"/api/repo/test": { connection: "github-app", role: "owner",
|
||||
source: { fullName: "owner/repo", branch: "main", commit: "saved-sha" }, options: { terms: [], update: false } },
|
||||
});
|
||||
expect([...ui.window.document.querySelectorAll("button")].some(b => b.textContent === "Repair GitHub access" || b.textContent === "Manage access")).to.equal(false);
|
||||
expect([...ui.window.document.querySelectorAll("button")].some(b => b.textContent === "Repair GitHub access" || b.textContent.trim() === "Manage GitHub access")).to.equal(false);
|
||||
});
|
||||
|
||||
for (const status of ["removed", "archived", "preparing", "queue", "download", "removing", "expiring", "ready", "error", "expired"]) {
|
||||
@@ -1677,6 +1674,12 @@ describe("Vue 3 UI", function () {
|
||||
"/api/repo/test/files/": [{ name: file, path: "", sha: "1", size: 5 }],
|
||||
});
|
||||
await delay(80);
|
||||
if (/\.csv$/.test(file)) {
|
||||
// CSV opens as a table; its Text view is the code editor.
|
||||
expect(ui.window.document.querySelector(".csv-table")).not.to.equal(null);
|
||||
ui.window.document.querySelector(".csv-toggle .seg-chip:last-child").click();
|
||||
await delay(80);
|
||||
}
|
||||
const host = ui.window.document.querySelector(".ace_editor");
|
||||
expect(host).not.to.equal(null);
|
||||
expect(ui.window.ace.edit(host).session.getMode().$id).to.equal("ace/mode/" + mode);
|
||||
@@ -1777,8 +1780,8 @@ describe("Vue 3 UI", function () {
|
||||
|
||||
it("leads a ready status page with the copyable anonymous link", async function () {
|
||||
ui = await browser("/status/test", { "/api/repo/test": { repoId: "test", status: "ready", options: {} } });
|
||||
const input = ui.window.document.querySelector(".share-link-input");
|
||||
expect(input.value).to.match(/\/r\/test\/$/);
|
||||
const link = ui.window.document.querySelector(".share-link-url");
|
||||
expect(link.textContent.trim()).to.match(/\/r\/test\/$/);
|
||||
expect(ui.window.document.querySelector(".paper-progress")).to.equal(null);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
@@ -1801,10 +1804,13 @@ describe("Vue 3 UI", function () {
|
||||
"/api/repo/test/files/": [{ name: filename, path: "", size: 1 }, { name: "constructor", path: "" }, { name: "index.js", path: "constructor", size: 2 }, { name: "hello.txt", path: "", size: 3 }],
|
||||
});
|
||||
const tree = ui.window.document.querySelector("tree");
|
||||
expect(tree.textContent).to.include(filename).and.include("constructor/index.js");
|
||||
// A folder holding a single file stays a folder (only folder chains are joined as "a/b").
|
||||
expect(tree.textContent).to.include(filename).and.include("constructor");
|
||||
expect(tree.querySelector('li.folder > a[data-path="/constructor"]')).to.not.equal(null);
|
||||
expect(ui.window.probe).to.equal(undefined);
|
||||
await ui.go("/r/test/constructor/index.js");
|
||||
expect(ui.window.document.querySelector("tree")).to.equal(tree);
|
||||
expect(tree.querySelector('li.active > a[data-path="/constructor/index.js"]')).to.not.equal(null);
|
||||
expect(ui.requests.some(r => r.url.pathname === "/api/repo/test/file/constructor/index.js")).to.equal(true);
|
||||
await ui.go("/r/other/hello.txt");
|
||||
expect(ui.requests.some(r => r.url.pathname === "/api/repo/other/options")).to.equal(true);
|
||||
@@ -2001,7 +2007,7 @@ describe("Vue 3 UI", function () {
|
||||
"/api/user/dashboard": { items: [{ _type: "repo", repoId: "package", status: "ready",
|
||||
source: { fullName: "owner/package", commit } }], total: 1, filtered: 1, attention: 0, cursor: null },
|
||||
});
|
||||
expect(ui.window.document.querySelector(".commit-hash").textContent).to.equal(commit.slice(0, 8));
|
||||
expect(ui.window.document.querySelector(".commit-hash").textContent).to.equal(commit.slice(0, 7));
|
||||
const state = ui.window.document.querySelector("#search")._field.binding.state;
|
||||
await state.showProjectDetails(state.items[0]);
|
||||
expect(ui.window.document.querySelector(".project-details").textContent).to.include(commit);
|
||||
|
||||
Reference in new issue
Block a user