Files
stupid-simple-network-inven…/docs/extending.md
T
olivier 88cf6458d0 Initial commit — Stupid Simple Network Inventory
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 09:19:19 +02:00

5.4 KiB

Extending the Application

Add a Field to Device

Every new Device field requires changes in four places. Missing any one of them will cause data loss or silent failures.

1. backend/models.py

Add the column to the Device class:

my_field = Column(String, nullable=True)

2. backend/main.py

Add an idempotent migration function and call it before Base.metadata.create_all:

def _migrate_device_my_field():
    with engine.connect() as conn:
        if not conn.execute(text(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='devices'"
        )).fetchone():
            return
        cols = [row[1] for row in conn.execute(text("PRAGMA table_info(devices)")).fetchall()]
        if 'my_field' not in cols:
            conn.execute(text("ALTER TABLE devices ADD COLUMN my_field VARCHAR"))
            conn.commit()

# Add the call at the bottom of the startup sequence:
_migrate_device_my_field()

3. backend/routers/devices.py

Add to both Pydantic models:

class DeviceCreate(BaseModel):
    my_field: Optional[str] = None

class DeviceOut(BaseModel):
    my_field: Optional[str] = None

Then assign explicitly in both create_device and update_device:

db_device = models.Device(
    ...
    my_field=device.my_field,
)

Do not use **device.model_dump() to populate the ORM object — it silently ignores any field not present in the constructor keyword args.

4. Frontend

Add the field to the device form in DeviceManager.vue and display it where needed (chip in TopologyGraph.vue, card in DeviceManager.vue). Add any new i18n keys to all three locales in i18n.js.


Add a New API Route Group

  1. Create backend/routers/myrouter.py following the pattern of vlans.py or devices.py.
  2. Import and register in backend/main.py:
    from routers import myrouter
    app.include_router(myrouter.router, prefix="/api/myroutes", tags=["myroutes"],
                       dependencies=[Depends(get_current_user)])
    
  3. Add the corresponding API object in frontend/src/api.js:
    export const myApi = {
      list:   ()         => http.get('/myroutes/'),
      create: (data)     => http.post('/myroutes/', data),
      remove: (id)       => http.delete(`/myroutes/${id}`),
    }
    

Add a New i18n Key

  1. Open frontend/src/i18n.js.
  2. Add the key to all three locale objects (fr, en, es).
  3. Use t('myKey') in the template.

For interpolation:

// i18n.js
fr: { myMsg: "Il y a {0} équipements dans {1}" }

// template
{{ tFmt('myMsg', devices.length, vlan.name) }}

Add a Language

  1. In frontend/src/i18n.js, add a new locale object with all existing keys translated.
  2. Add the language code to the valid values type/comment.
  3. In App.vue, add a pill button to the language switcher:
    <button :class="{ active: locale === 'de' }" @click="setLocale('de')">de</button>
    

Add a Brand Icon

Supported brands are detected by keyword-matching device name and description.

  1. Find the Simple Icons export name:
    cd frontend
    node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('yourterm')))"
    
  2. In frontend/src/brandIcons.js, import the icon:
    import { siYourbrand } from 'simple-icons'
    
  3. Add an entry to the BRANDS array:
    {
      id: 'yourbrand',
      name: 'YourBrand',
      keywords: ['yourbrand', 'alternate-name'],
      hex: siYourbrand.hex,
      path: siYourbrand.path,
    }
    

keywords are matched case-insensitively against the device name and description fields.


Add a Vue Component

  1. Create frontend/src/components/MyComponent.vue using <script setup> syntax.
  2. Use CSS custom properties for all colors (see docs/frontend.md for the variable list).
  3. Use t('key') for all visible strings — never hardcode text.
  4. For dark-mode scoped overrides, put the full selector inside :global():
    :global(html.dark .my-component) { background: #1E293B; }
    
  5. Import and use in App.vue or the relevant parent component.

Change the Default Admin Password

The admin account is seeded by _migrate_users() only when the users table is empty. To reset credentials on a running instance, use the account settings modal in the UI (sidebar footer → account icon).

To reset programmatically (e.g., locked out):

docker compose exec backend python3 -c "
from database import engine
from sqlalchemy import text
from passlib.context import CryptContext
pwd = CryptContext(schemes=['bcrypt'], deprecated='auto')
with engine.connect() as conn:
    conn.execute(text(\"UPDATE users SET hashed_password=:h WHERE username='admin'\"), {'h': pwd.hash('newpassword')})
    conn.commit()
"

Upgrade Dependencies

Backend Python packages

Check for passlib/bcrypt compatibility before upgrading:

  • passlib 1.7.4 requires bcrypt < 4.0 — see requirements.txt for the pin.
  • If upgrading passlib, verify the new version supports the installed bcrypt before removing the pin.

Frontend npm packages

simple-icons has breaking changes between major versions (icon names and SVG paths change). Pin the major version in package.json and test brand detection after upgrading.

lucide-vue-next is generally backwards-compatible but icons are occasionally renamed or removed.