AgentSkillsCN

coding-standards

StakTrakr编码标准——采用原生JavaScript,构建单页应用,无需构建步骤,使用localStorage进行持久化存储。

SKILL.md
--- frontmatter
name: coding-standards
description: StakTrakr coding standards — vanilla JavaScript, single-page app, no build step, localStorage persistence.

StakTrakr Coding Standards

Coding standards for a pure client-side precious metals inventory tracker. Single HTML page (index.html), vanilla JavaScript, localStorage persistence, no backend, no build step. Must work on both file:// protocol and HTTP servers.


1. Architecture & Project Structure

Single-page, no-build design

All JavaScript loads via <script> tags in index.html. There are no ES modules, no bundler, no transpilation. Every top-level const, let, function, and class declaration is a global. Treat the global namespace as the module system.

Script loading order (mandatory)

Scripts load in strict dependency order defined in index.html. file-protocol-fix.js loads first (no defer), init.js loads last. Breaking this order causes ReferenceError on undefined globals.

When adding a new script file:

  1. Identify which existing globals it depends on
  2. Place the <script> tag after all dependencies in index.html
  3. If the new file defines globals others need, place it before those consumers

Module boundaries

Each .js file owns a specific domain. Respect boundaries:

FileResponsibility
constants.jsConfiguration, API providers, storage keys, feature flags
state.jsAll mutable application state, cached DOM references
utils.jsFormatting, validation, storage helpers, error handling
events.jsEvent binding, modal submit handlers, UI interactions
inventory.jsCRUD operations, table rendering, CSV/PDF/ZIP export
api.jsExternal pricing API calls with provider fallback
init.jsApplication bootstrap (runs last)

Don't put DOM event handlers in utils.js. Don't put formatting helpers in events.js. Don't put state declarations outside state.js.

Global scope discipline

Since everything is global, follow these rules to avoid collisions:

  • Constants: define with const at file top, expose via window.X = X block at file bottom
  • State variables: declare only in state.js with let
  • Helper functions: define with const fn = () => {} in their owning module
  • No generic names: avoid data, result, temp, value at file scope. Prefix with domain context (e.g., apiCache, spotHistory)

2. Code Style

Formatting

  • 2-space indentation (no tabs)
  • Semicolons always — every statement ends with ;
  • Trailing commas in multi-line arrays and objects
  • 120-character soft line limit — break long lines at logical points

Quotes

The codebase uses mixed quote styles. The convention:

  • Double quotes for string values in configuration objects and data ("silver", "USD")
  • Single quotes for DOM selectors, localStorage keys, and template fragments ('changeLog', 'hidden.bs.modal')
  • Template literals for string interpolation and multi-line strings — always prefer over concatenation

Variable declarations

javascript
// const-first — use for everything that isn't reassigned
const API_CACHE_DURATION = 24 * 60 * 60 * 1000;
const formatCurrency = (value) => `$${value.toFixed(2)}`;

// let — only when the value changes
let editingIndex = null;
let sortDirection = "desc";

// NEVER use var — it leaks scope and hoists unpredictably

Naming conventions

KindConventionExamples
Variables & functionscamelCasesortColumn, editingIndex, formatCurrency
ConstantsUPPER_SNAKE_CASEAPP_VERSION, LS_KEY, DEFAULT_CURRENCY
ClassesPascalCaseFeatureFlags
Fileskebab-casefile-protocol-fix.js, debug-log.js
CSS classeskebab-casefilter-text, na-value, spot-card
DOM IDscamelCasespotSilver, itemModal, inventoryTable
localStorage keyscamelCase or dot.notationmetalInventory, staktrakr.catalog.cache

Functions

  • Arrow functions for callbacks, inline handlers, and short helpers
  • function declarations for hoisted functions that need to be called before definition (rare — prefer const)
  • Verb-noun naming: saveData, loadData, formatCurrency, handleError, renderTable
  • Boolean-returning functions: prefix with is, has, can, should (e.g., isFeatureEnabled)

Comparison

  • Strict equality only: === and !==. Never use == or !=
  • Nullish checks: prefer value == null (the one exception to strict equality — catches both null and undefined) or optional chaining (data?.rates?.price)

3. DOM Interaction

Element access

Always use safeGetElement(id) instead of raw document.getElementById(). It prevents null reference errors and provides optional warning logging:

javascript
// CORRECT
const el = safeGetElement("inventoryTable");

// WRONG — crashes if element doesn't exist
const el = document.getElementById("inventoryTable");

Cached DOM references

Frequently accessed elements are cached in the elements object in state.js. Use these cached references instead of repeated lookups:

javascript
// CORRECT — use cached reference
elements.inventoryTable.innerHTML = "";

// WRONG — redundant DOM query
document.getElementById("inventoryTable").innerHTML = "";

When adding a new persistent UI element, add its reference to the elements object in state.js and initialize it during initializeElements() in init.js.

Event binding

Use safeAttachListener() from events.js for all event binding. It handles null elements, provides fallback binding methods, and logs failures:

javascript
safeAttachListener(elements.searchInput, "input", handleSearch, "search input");

Content injection

MethodWhen to use
textContentDefault for all text. Safe, no XSS risk
innerHTML with escapeAttribute()When HTML structure is needed AND content includes user data
innerHTML with static HTMLOnly for trusted, hardcoded markup with no user content
classList.add/remove/toggleAll class manipulation — never string-manipulate className
javascript
// CORRECT — safe text injection
el.textContent = item.name;

// CORRECT — escaped user content in HTML
el.innerHTML = `<span title="${escapeAttribute(item.notes)}">${escapeAttribute(item.name)}</span>`;

// WRONG — XSS vulnerability
el.innerHTML = `<span>${item.name}</span>`;

4. State & Data Flow

Application state

All mutable state lives in state.js as top-level let declarations:

javascript
let inventory = [];           // Main data
let spotPrices = { silver: 0, gold: 0, platinum: 0, palladium: 0 };
let editingIndex = null;      // UI state

Never declare mutable state in other files. If a new module needs persistent state, add the variable to state.js.

Data persistence

Use the async storage helpers from utils.js:

javascript
// PREFERRED — async with compression
await saveData(LS_KEY, inventory);
const data = await loadData(LS_KEY, []);

// LEGACY — sync versions (use only when async isn't possible)
saveDataSync(LS_KEY, inventory);
const data = loadDataSync(LS_KEY, []);

Never use localStorage.getItem() / localStorage.setItem() directly for application data. The helpers handle JSON serialization, compression, and error recovery.

localStorage key whitelist

Every localStorage key must be registered in ALLOWED_STORAGE_KEYS in constants.js before use. The security cleanup routine (cleanupLocalStorage) deletes any key not in this list. Forgetting to register a key means data loss on next cleanup.

Mutation cycle

The standard data flow is: mutate state → save to storage → re-render UI

javascript
// Example: delete an item
inventory.splice(index, 1);
await saveData(LS_KEY, inventory);
renderTable();

Never re-render without saving first (stale UI on reload). Never save without re-rendering (stale UI until reload).


5. Error Handling

Storage and API operations

Wrap all localStorage and fetch calls in try/catch:

javascript
try {
  const data = await loadData(LS_KEY, []);
  // ... use data
} catch (error) {
  console.error("[inventory] Failed to load data:", error);
  // Fall back to empty state
  inventory = [];
}

User-facing errors

Use handleError(error, context) from utils.js for errors that need user notification:

javascript
handleError(error, "CSV import");
// Currently shows alert() — will migrate to toast notifications

Note: The codebase currently uses alert() for error display. New code should still use handleError() to centralize the migration path to toast notifications.

Console logging

Use contextual prefixes for all console output:

javascript
console.error("[api] Fetch failed for silver:", error);
console.warn("[inventory] Missing weight field, defaulting to 1oz");
console.log("[spot] Cache hit for gold, age: 2h");

Debug logging

Use debugLog() for development tracing that should be silent in production:

javascript
debugLog("Rendering table with", inventory.length, "items");
// Only outputs when DEBUG === true (set via DEV_MODE or ?debug URL param)

Fallback defaults

Always provide sensible defaults rather than crashing:

javascript
const price = parseFloat(item.price) || 0;
const weight = item.weight || 1;
const name = item.name || "Unknown Item";

6. Security

Storage key whitelist

All keys in ALLOWED_STORAGE_KEYS before first use. The security cleanup routine runs on app init and will delete unregistered keys.

HTML escaping

Use escapeAttribute() (defined in inventory.js) on all user-provided content injected into HTML:

javascript
// Name, notes, locations, serial numbers — anything the user typed
const safe = escapeAttribute(item.name);

Prefer textContent over innerHTML wherever HTML structure isn't required. When building HTML strings with user content, every interpolated value must be escaped.

Import sanitization

All CSV and JSON imports pass through sanitizeImportedItem() from utils.js. This function:

  • Strips unexpected fields
  • Validates and coerces types
  • Applies default values for missing fields
  • Prevents injection via crafted import files

Input validation

Validate at form boundaries (the add/edit modal submit handler). Internal functions may trust their inputs since data has already been sanitized on entry.


7. API Integration

Provider fallback chain

The pricing API uses a ranked provider list. If the primary fails, the system falls through to backups:

javascript
// Pattern: try providers in order, return first success
for (const provider of orderedProviders) {
  try {
    const result = await fetchFromProvider(provider);
    if (result) return result;
  } catch (error) {
    console.warn(`[api] ${provider.name} failed, trying next:`, error.message);
  }
}

Caching

API responses are cached with TTL per provider (default: 24 hours). Always check cache before making a network request. Cache keys and timestamps are stored in localStorage.

Error recovery

API errors must be:

  1. Logged with provider name and context
  2. Recovered via fallback provider or cached data
  3. Never silent — if all providers fail, notify the user

Async style

Use async/await for all asynchronous operations. Never use .then() chains:

javascript
// CORRECT
const response = await fetch(url);
const data = await response.json();

// WRONG — don't mix callback style
fetch(url).then(res => res.json()).then(data => { ... });

8. Library-Specific Standards

Chart.js

  • Destroy before reuse: call .destroy() on existing chart instance before creating a new one on the same canvas. Failing to destroy causes memory leaks and ghost overlays
  • Disable animations on programmatic updates (when the user didn't trigger the render)
  • Store instances in the chartInstances or sparklineInstances objects in state.js
javascript
if (chartInstances.typeChart) {
  chartInstances.typeChart.destroy();
}
chartInstances.typeChart = new Chart(canvas, config);

Bootstrap 5

  • getOrCreateInstance() instead of new bootstrap.Modal() — prevents duplicate instance errors
  • Dispose after hidden.bs.* event when modals are dynamically created
  • Never mix jQuery and Bootstrap 5 — the app uses vanilla Bootstrap only
  • Use data-bs-* attributes for declarative behavior, JavaScript API for programmatic control

PapaParse (CSV)

  • Always check results.errors after parsing — PapaParse can return partial data with errors
  • Use skipEmptyLines: 'greedy' to handle trailing newlines and whitespace-only rows
  • Two-tier validation: PapaParse structural errors first, then business logic validation on each row

jsPDF + AutoTable

  • Use AutoTable for all tabular exports — never manually position text for table layouts
  • Optimize images before embedding — large base64 images bloat PDF size
  • Set page orientation based on column count (portrait for narrow tables, landscape for wide)

JSZip

  • generateAsync({type: "blob"}) for browser downloads — never use synchronous generation
  • Use file() method for adding entries, not direct property assignment

9. Anti-Patterns

Things to actively avoid and fix when encountered during refactoring:

Never do these

Anti-patternDo this instead
var x = ...const x = ... or let x = ...
== or !==== or !== (except == null for nullish check)
localStorage.getItem() for app dataloadData() / loadDataSync()
localStorage.setItem() for app datasaveData() / saveDataSync()
document.getElementById()safeGetElement()
innerHTML = userContenttextContent = userContent or escapeAttribute()
.then().catch() chainsasync/await with try/catch
element.className = "..."element.classList.add/remove/toggle()
new bootstrap.Modal(el)bootstrap.Modal.getOrCreateInstance(el)

Avoid these patterns

  • Nested ternaries deeper than 2 levels — use if/else or extract a helper function
  • Magic numbers — define in constants.js with a descriptive name
  • Mixed sync/async storage in the same function — pick one style per function
  • Silent error swallowingcatch (e) {} with no logging or fallback
  • DOM queries in loops — cache the element reference before the loop
  • String concatenation for HTML without escaping — always use escapeAttribute() on user content
  • Hardcoded localStorage key strings — use the named constants from constants.js