Compare commits
5 commits
d113b7821c
...
fe43f67b51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe43f67b51 | ||
|
|
ab83f5fd2b | ||
|
|
d9aa6bd395 | ||
|
|
e75feb0649 | ||
|
|
358a835d65 |
5 changed files with 207 additions and 0 deletions
131
docs/superpowers/plans/2026-05-13-rcon-password-display-v1.md
Normal file
131
docs/superpowers/plans/2026-05-13-rcon-password-display-v1.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# RCON Password Display Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Show the RCON password on the server detail page with a show/hide toggle.
|
||||
|
||||
**Architecture:** Three-file change. An external JS file (`password-reveal.js`) provides the reveal/hide interaction via event delegation on `[data-password-toggle]` attributes — no inline handlers or HTML event attributes. The template adds a row to the existing `.server-info` definition list with a masked span, value span, and toggle button. Base.html adds the script include alongside existing JS files.
|
||||
|
||||
**Tech Stack:** Vanilla JS, Jinja2 templates, Flask
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `l4d2web/static/js/password-reveal.js` | New. Delegated click listener for show/hide toggle on `[data-password-toggle]` |
|
||||
| `l4d2web/templates/server_detail.html` | Add one `<div>` row to `.server-info` DL |
|
||||
| `l4d2web/templates/base.html` | Add `<script src="...password-reveal.js">` |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create the reveal/hide JS
|
||||
|
||||
**Files:**
|
||||
- Create: `l4d2web/static/js/password-reveal.js`
|
||||
|
||||
- [ ] **Step 1: Create `password-reveal.js`**
|
||||
|
||||
```js
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-password-toggle]');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.passwordToggle;
|
||||
const mask = document.querySelector(`[data-password-field="${id}"].password-mask`);
|
||||
const value = document.querySelector(`[data-password-field="${id}"].password-value`);
|
||||
if (!mask || !value) return;
|
||||
const hidden = value.hidden;
|
||||
value.hidden = !hidden;
|
||||
mask.hidden = hidden;
|
||||
btn.textContent = hidden ? 'hide' : 'show';
|
||||
btn.setAttribute('aria-label', hidden ? 'Hide RCON password' : 'Show RCON password');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file exists**
|
||||
|
||||
Run: `ls -la l4d2web/static/js/password-reveal.js`
|
||||
Expected: File exists, is about 450 bytes
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add l4d2web/static/js/password-reveal.js
|
||||
git commit -m "feat: add password reveal toggle JS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add RCON password row to server detail template
|
||||
|
||||
**Files:**
|
||||
- Modify: `l4d2web/templates/server_detail.html:13`
|
||||
|
||||
- [ ] **Step 1: Add the RCON password row after the blueprint row**
|
||||
|
||||
Insert after line 13 (`</dd></div>` for blueprint):
|
||||
|
||||
```html
|
||||
<div><dt>RCON Password</dt><dd><span class="password-mask" data-password-field="{{ server.id }}">••••••••••••</span><span class="password-value" data-password-field="{{ server.id }}" hidden>{{ server.rcon_password }}</span> <button class="link-button" data-password-toggle="{{ server.id }}" aria-label="Show RCON password">show</button></dd></div>
|
||||
```
|
||||
|
||||
Expected result: the `.server-info` DL now shows three rows: Port, Blueprint, RCON Password.
|
||||
|
||||
- [ ] **Step 2: Verify template renders**
|
||||
|
||||
Run: `python -c "from jinja2 import Environment; env=Environment(); env.parse(open('l4d2web/templates/server_detail.html').read()); print('parse ok')"`
|
||||
Expected: `parse ok`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add l4d2web/templates/server_detail.html
|
||||
git commit -m "feat: add RCON password row to server detail page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Include the script in base template
|
||||
|
||||
**Files:**
|
||||
- Modify: `l4d2web/templates/base.html:44`
|
||||
|
||||
- [ ] **Step 1: Add the script include**
|
||||
|
||||
Insert after line 43 (`<script src="...file-tree.js">`):
|
||||
|
||||
```html
|
||||
<script src="{{ url_for('static', filename='js/password-reveal.js') }}"></script>
|
||||
```
|
||||
|
||||
Expected result: `base.html` now has 5 script includes: htmx, csrf.js, sse.js, modal.js, file-tree.js, password-reveal.js.
|
||||
|
||||
- [ ] **Step 2: Verify the app starts**
|
||||
|
||||
Run: `cd l4d2web && python -c "from l4d2web.app import create_app; app=create_app(); print('app created ok')"` (or similar smoke test)
|
||||
|
||||
Expected: App initializes without import/template errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add l4d2web/templates/base.html
|
||||
git commit -m "feat: include password-reveal.js in base template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Run tests
|
||||
|
||||
**Files:** None
|
||||
|
||||
- [ ] **Step 1: Run existing test suite**
|
||||
|
||||
Run: `pytest l4d2web/tests -q`
|
||||
Expected: All tests pass (no regressions from this purely-presentational change)
|
||||
|
||||
- [ ] **Step 2: If any tests fail, investigate and fix**
|
||||
|
||||
Run: `pytest l4d2web/tests -q --tb=long`
|
||||
Expected: Clear failure report to debug
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# RCON Password Display on Server Detail Page — Design
|
||||
|
||||
**Goal:** Show the RCON password on the server detail page with a show/hide toggle.
|
||||
|
||||
**Architecture:** Presentational change only. The `server.rcon_password` field already exists in the database and is rendered via Jinja2 autoescaping into the template. A small external JS file provides the reveal/hide interaction via delegated click on `[data-password-toggle]` attributes — no inline handlers.
|
||||
|
||||
**Files touched:**
|
||||
- `l4d2web/static/js/password-reveal.js` — new, ~15 lines
|
||||
- `l4d2web/templates/server_detail.html` — add one row to `.server-info` DL
|
||||
- `l4d2web/templates/base.html` — add script include
|
||||
- `l4d2web/static/css/components.css` — optional, add `.password-mask` letter-spacing if default renders poorly
|
||||
|
||||
---
|
||||
|
||||
## Template
|
||||
|
||||
Add after the blueprint row in `server_detail.html` (line 13):
|
||||
|
||||
```html
|
||||
<div>
|
||||
<dt>RCON Password</dt>
|
||||
<dd>
|
||||
<span class="password-mask" data-password-field="{{ server.id }}">••••••••••••</span>
|
||||
<span class="password-value" data-password-field="{{ server.id }}" hidden>{{ server.rcon_password }}</span>
|
||||
<button class="link-button" data-password-toggle="{{ server.id }}" aria-label="Show RCON password">show</button>
|
||||
</dd>
|
||||
</div>
|
||||
```
|
||||
|
||||
## JavaScript (`password-reveal.js`)
|
||||
|
||||
Delegated click listener on `[data-password-toggle]`. Toggles `hidden` between the mask span and value span, updates button text and aria-label.
|
||||
|
||||
```js
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-password-toggle]');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.passwordToggle;
|
||||
const mask = document.querySelector(`[data-password-field="${id}"].password-mask`);
|
||||
const value = document.querySelector(`[data-password-field="${id}"].password-value`);
|
||||
const hidden = value.hidden;
|
||||
value.hidden = !hidden;
|
||||
mask.hidden = hidden;
|
||||
btn.textContent = hidden ? 'hide' : 'show';
|
||||
btn.setAttribute('aria-label', hidden ? 'Hide RCON password' : 'Show RCON password');
|
||||
});
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
Reuse existing `.link-button` for the toggle button. If the bullet characters render inconsistently across browsers (spacing, baseline), add a simple `.password-mask { letter-spacing: 0.15em; }` class — but likely unnecessary.
|
||||
|
||||
## Security
|
||||
|
||||
- Password is server-rendered via Jinja2 autoescaping — no XSS vector.
|
||||
- Visible in page source to the server owner (consistent with existing auth model: user must own the server).
|
||||
- No copy-to-clipboard functionality (per requirements).
|
||||
|
||||
## Testing
|
||||
|
||||
No new tests required — purely presentational change. Existing `test_create_server_generates_rcon_password` in `test_servers.py` already covers password generation.
|
||||
13
l4d2web/static/js/password-reveal.js
Normal file
13
l4d2web/static/js/password-reveal.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-password-toggle]');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.passwordToggle;
|
||||
const mask = document.querySelector(`[data-password-field="${id}"].password-mask`);
|
||||
const value = document.querySelector(`[data-password-field="${id}"].password-value`);
|
||||
if (!mask || !value) return;
|
||||
const hidden = value.hidden;
|
||||
value.hidden = !hidden;
|
||||
mask.hidden = hidden;
|
||||
btn.textContent = hidden ? 'hide' : 'show';
|
||||
btn.setAttribute('aria-label', hidden ? 'Hide RCON password' : 'Show RCON password');
|
||||
});
|
||||
|
|
@ -41,5 +41,6 @@
|
|||
<script src="{{ url_for('static', filename='js/sse.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/file-tree.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/password-reveal.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<dl class="server-info">
|
||||
<div><dt>Port</dt><dd><a href="steam://run/550//+connect%20{{ connect_host }}:{{ server.port }}">{{ server.port }}</a></dd></div>
|
||||
<div><dt>Blueprint</dt><dd>{% if blueprint %}<a href="/blueprints/{{ blueprint.id }}">{{ blueprint.name }}</a>{% endif %}</dd></div>
|
||||
<div><dt>RCON Password</dt><dd><span class="password-mask" data-password-field="{{ server.id }}">••••••••••••</span><span class="password-value" data-password-field="{{ server.id }}" hidden>{{ server.rcon_password }}</span> <button class="link-button" data-password-toggle="{{ server.id }}" aria-label="Show RCON password">show</button></dd></div>
|
||||
</dl>
|
||||
|
||||
<h2 class="section-title">Actions</h2>
|
||||
|
|
|
|||
Loading…
Reference in a new issue