Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 68 additions & 32 deletions generate-index.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ func main() {
os.Exit(1)
}

if err := generate404(); err != nil {
fmt.Fprintf(os.Stderr, "Error generating 404.html: %v\n", err)
os.Exit(1)
}

// Copy CNAME and static root files to site directory
if cname, err := os.ReadFile("CNAME"); err == nil {
os.WriteFile("site/CNAME", cname, 0644)
Expand All @@ -71,7 +76,14 @@ func main() {
for _, cat := range cfg.Categories {
totalRepos += len(cat.Repos)
}
fmt.Printf("Generated index.html and sitemap.xml (%d repos)\n", totalRepos)
fmt.Printf("Generated site (%d repos)\n", totalRepos)
}

func generate404() error {
return os.WriteFile("site/404.html", []byte(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=/"><title>Redirecting…</title></head>
<body><p>Redirecting to <a href="/">homepage</a>…</p></body></html>
`), 0644)
}

func generateSitemap(cfg Config) error {
Expand Down Expand Up @@ -196,18 +208,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
.site-nav { display: flex; gap: 16px; align-items: center; }
.site-nav a { color: var(--text-muted); font-size: 14px; font-weight: 500; white-space: nowrap; }
.site-nav a:hover { color: var(--text); text-decoration: none; }
.request-btn {
color: var(--accent-light) !important;
border: 1px solid var(--accent);
border-radius: var(--radius);
padding: 6px 12px;
transition: background 0.2s, color 0.2s;
}
.request-btn:hover {
background: var(--accent);
color: #fff !important;
text-decoration: none !important;
}
.hero {
padding: 64px 0 48px;
text-align: center;
Expand Down Expand Up @@ -395,14 +395,21 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
pointer-events: auto;
}
.submit-btn.active:hover { background: var(--accent-light); }
.submit-preview {
margin-top: 8px;
.submit-btn.loading {
opacity: 0.6;
pointer-events: none;
}
.submit-feedback {
margin-top: 10px;
font-size: 13px;
color: var(--green);
font-family: var(--mono);
display: none;
}
.submit-preview.visible { display: block; }
.submit-feedback.visible { display: block; }
.submit-feedback.preview { color: var(--text-muted); }
.submit-feedback.success { color: var(--green); }
.submit-feedback.success a { color: var(--green); text-decoration: underline; }
.submit-feedback.error { color: var(--red); }
@media (max-width: 768px) {
.container { padding: 0 16px; }
.hero { padding: 40px 0 32px; }
Expand Down Expand Up @@ -432,7 +439,6 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
<a href="https://supermodeltools.com">Website</a>
<a href="https://github.com/supermodeltools">GitHub</a>
<a href="https://x.com/supermodeltools">X</a>
<a href="https://github.com/supermodeltools/supermodeltools.github.io/issues/new?template=request-repo.yml" class="request-btn">+ Request a Repo</a>
</nav>
</div>
</header>
Expand Down Expand Up @@ -462,9 +468,9 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
<div class="submit-label">Don't see your repo? Paste a URL to generate arch docs:</div>
<div class="submit-row">
<input type="text" class="submit-input" id="submit-url" placeholder="https://github.com/owner/repo" autocomplete="off" spellcheck="false">
<button class="submit-btn" id="submit-btn" type="button">Request</button>
<button class="submit-btn" id="submit-btn" type="button">Generate</button>
</div>
<div class="submit-preview" id="submit-preview"></div>
<div class="submit-feedback" id="submit-feedback"></div>
</div>
</div>

Expand Down Expand Up @@ -510,10 +516,10 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
var noResults = document.getElementById('no-results');
var submitInput = document.getElementById('submit-url');
var submitBtn = document.getElementById('submit-btn');
var submitPreview = document.getElementById('submit-preview');
var feedback = document.getElementById('submit-feedback');
var noResultsRequest = document.getElementById('no-results-request');

var issueBase = 'https://github.com/supermodeltools/supermodeltools.github.io/issues/new?template=request-repo.yml';
var API_URL = '/api/request';

// --- Search ---
searchInput.addEventListener('input', function() {
Expand Down Expand Up @@ -546,36 +552,65 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
return null;
}

function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.innerHTML = msg;
}
Comment on lines +555 to +558
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid innerHTML for feedback messages.

Line [554] uses innerHTML for text/status content. If backend error strings ever contain HTML, this becomes an XSS sink. Use plain text rendering here.

Suggested fix
 function showFeedback(msg, type) {
   feedback.className = 'submit-feedback visible ' + type;
-  feedback.innerHTML = msg;
+  feedback.textContent = msg;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.innerHTML = msg;
}
function showFeedback(msg, type) {
feedback.className = 'submit-feedback visible ' + type;
feedback.textContent = msg;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-index.go` around lines 552 - 555, The showFeedback function
currently assigns feedback.innerHTML = msg which can introduce an XSS sink if
msg contains HTML; change it to render plain text instead (e.g., use
feedback.textContent = msg or create a text node and append it) and keep the
className assignment as-is; update the showFeedback function reference so any
callers still pass a plain string and remove reliance on innerHTML for
displaying backend error strings.


submitInput.addEventListener('input', function() {
var parsed = parseRepo(this.value);
if (parsed) {
var name = parsed.split('/')[1];
submitPreview.textContent = '\u2192 Docs will be at repos.supermodeltools.com/' + name + '/';
submitPreview.classList.add('visible');
showFeedback('\u2192 repos.supermodeltools.com/' + name + '/', 'preview');
submitBtn.classList.add('active');
} else {
submitPreview.classList.remove('visible');
feedback.className = 'submit-feedback';
submitBtn.classList.remove('active');
}
});

function submitRequest() {
async function submitRequest() {
var parsed = parseRepo(submitInput.value);
if (!parsed) return;

var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
var url = issueBase
+ '&repo_url=' + encodeURIComponent(repoUrl)
+ '&title=' + encodeURIComponent('[Repo Request] ' + name);
window.open(url, '_blank');

// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Generating...';
showFeedback('Setting up ' + name + '...', 'preview');

try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();

if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}

// Redirect to the skeleton loading page — served by the worker
window.location.href = data.generating_url;
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
}
}

submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitRequest();
if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
});
Comment on lines +572 to 611
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Block re-entrancy to prevent duplicate repo requests.

Right now, Line [607] can still fire submitRequest() while the button is already loading, and submitRequest() itself has no guard. Since this endpoint creates GitHub issues (non-idempotent), duplicate submits are a real risk.

Suggested fix
 async function submitRequest() {
+  if (submitBtn.classList.contains('loading')) return;
   var parsed = parseRepo(submitInput.value);
   if (!parsed) return;
@@
 submitInput.addEventListener('keydown', function(e) {
-  if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
+  if (e.key === 'Enter'
+      && submitBtn.classList.contains('active')
+      && !submitBtn.classList.contains('loading')) {
+    submitRequest();
+  }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function submitRequest() {
var parsed = parseRepo(submitInput.value);
if (!parsed) return;
var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
var url = issueBase
+ '&repo_url=' + encodeURIComponent(repoUrl)
+ '&title=' + encodeURIComponent('[Repo Request] ' + name);
window.open(url, '_blank');
// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Generating...';
showFeedback('Setting up ' + name + '...', 'preview');
try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();
if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}
// Redirect to the docs page — 404.html shows loading until docs are ready
window.location.href = data.docs_url;
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
}
}
submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitRequest();
if (e.key === 'Enter' && submitBtn.classList.contains('active')) submitRequest();
});
async function submitRequest() {
if (submitBtn.classList.contains('loading')) return;
var parsed = parseRepo(submitInput.value);
if (!parsed) return;
var repoUrl = 'https://github.com/' + parsed;
var name = parsed.split('/')[1];
// Loading state
submitBtn.classList.add('loading');
submitBtn.textContent = 'Generating...';
showFeedback('Setting up ' + name + '...', 'preview');
try {
var resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: repoUrl }),
});
var data = await resp.json();
if (!resp.ok || !data.success) {
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
return;
}
// Redirect to the docs page — 404.html shows loading until docs are ready
window.location.href = data.docs_url;
} catch (e) {
showFeedback('Network error. Please try again.', 'error');
submitBtn.classList.remove('loading');
submitBtn.textContent = 'Generate';
}
}
submitBtn.addEventListener('click', submitRequest);
submitInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter'
&& submitBtn.classList.contains('active')
&& !submitBtn.classList.contains('loading')) {
submitRequest();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-index.go` around lines 569 - 608, submitRequest can be re-entered
while a request is in flight, causing duplicate non-idempotent submissions; add
a simple guard (e.g., an isSubmitting boolean) near the top of submitRequest and
return early if true, set isSubmitting = true when starting and set it back to
false in all exit paths (success, non-ok/data.error, and catch), and also
prevent the Enter-key handler from invoking submitRequest when isSubmitting is
true (or check submitBtn.classList.contains('loading') there); reference
submitRequest, submitBtn, submitInput, parseRepo, and API_URL when locating
where to add the guard and to ensure the loading state/reset is handled
consistently.


// "No results" request link: pre-fill with search query as a guess
// "No results" link: scroll up and focus the submit input
noResultsRequest.addEventListener('click', function() {
var q = searchInput.value.trim();
submitInput.value = q;
Expand All @@ -588,3 +623,4 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
</body>
</html>
`

Loading
Loading