Skip to content

Latest commit

 

History

History
306 lines (250 loc) · 19.9 KB

File metadata and controls

306 lines (250 loc) · 19.9 KB

Fluidd AI Development Guide

Fluidd is a Vue 2.7 + TypeScript web interface for Klipper 3D printers that communicates with Moonraker via WebSocket.

Architecture Overview

  • Vue 2.7 + Vuetify 2: UI framework with Material Design components
  • Vuex Store: 28 namespaced modules mirroring Klipper/Moonraker domains (printer/, files/, console/, macros/, webcams/, mmu/, spoolman/, etc.)
  • WebSocket Communication: Real-time JSON-RPC via custom WebSocketClient in src/plugins/socketClient.ts
  • Component Structure: Class-style components with vue-property-decorator; mixins-based architecture with StateMixin providing common printer state access

Key Patterns

Component Architecture

All components use class-style decorators — no Options API or Composition API:

// Standard component
@Component({ components: { /* ... */ } })
export default class MyComponent extends Vue {
  @Prop({ type: String, required: true })
  readonly label!: string

  @VModel({ type: Boolean })
  open?: boolean
}

// Component needing printer state — extend via Mixins()
@Component({ components: { /* ... */ } })
export default class PrinterWidget extends Mixins(StateMixin) {
  get klippyReady (): boolean {
    return this.$typedGetters['printer/getKlippyReady']
  }
}

Available mixins (src/mixins/): StateMixin, FilesMixin, ServicesMixin, BrowserMixin, CameraMixin, ToolheadMixin, AfcMixin, MmuMixin

State Management

  • Store modules in src/store/ — each has state.ts, getters.ts, mutations.ts, actions.ts, types.ts
  • Use $typedState and $typedGetters for type-safe store access (defined in src/plugins/filters.ts)
  • Use Vue.set() for reactive dynamic state properties
  • Module definition: export const auth = { namespaced, state, getters, actions, mutations } satisfies Module<AuthState, RootState>

WebSocket Integration

  • All printer communication through SocketActions in src/api/socketActions.ts (not direct HTTP)
  • Pattern: baseEmit<T>(method, { dispatch, wait, params })
  • Use wait parameter for UI loading states: wait: Waits.onPrintStart
  • Wait constants defined in src/globals.ts (Waits object, ~90 operation types)
  • Real-time updates handled via store mutations from socket events
  • Auto-reconnect with configurable interval (Globals.SOCKET_RETRY_DELAY)

Component Registration

  • Auto-imported (no manual import needed): components in src/components/common/, layout/, ui/ — via unplugin-vue-components with VuetifyResolver
  • Manual import required: widget components, view components
  • Lazy-loaded: EChart via Vue.component('EChart', () => import('./vue-echarts-chunk'))
  • Generated types: components.d.ts at repo root — auto-generated, do not edit manually

Development Workflow

Build Toolchain

  • Node.js 24 — pinned in .node-version (engines: ^22.12.0 || ^24)
  • Vite 8 — build tool and dev server
  • @pedrolamas/plugin-vue2 — Vue 2 SFC support for Vite
  • unplugin-vue-components/rolldown — auto-imports components from src/components/common|layout|ui
  • sass-embedded — SCSS preprocessor (variables auto-injected via @/scss/variables)
  • vitest v4 — unit test runner (jsdom environment)
  • commit-and-tag-version — release versioning (npm run release)
  • ESLint flat config (eslint.config.mjs) — enforced at dev time via vite-plugin-checker with useFlatConfig: true
  • vite-plugin-checker — runs vue-tsc and ESLint during dev (disabled at build time)
  • skott — circular dependency detection (npm run circular-check)
  • ES2020 lib target (tsconfig.app.json) — no ES2021+ built-ins without polyfills

Essential Commands

npm run bootstrap      # Install git hooks (after clone)
npm run dev            # Start development server (port 8080)
npm run build          # Production build
npm run type-check     # TypeScript validation (vue-tsc)
npm run lint           # ESLint with Vue/TS rules
npm run test           # Vitest unit tests
npm run circular-check # Check for circular dependencies

File Organization

src/
├── api/                # WebSocket (custom JSON-RPC) client (`socketActions.ts`)
├── components/
│   ├── common/         # Shared dialogs & status components (auto-imported)
│   ├── layout/         # App shell: AppBar, AppDrawer, etc. (auto-imported)
│   ├── settings/       # Settings page components
│   ├── ui/             # Reusable: AppBtn, AppDialog, AppChart (auto-imported)
│   └── widgets/        # 27 feature widget dirs: bedmesh/, camera/, console/, filesystem/, macros/, mmu/, thermals/, toolhead/, etc.
├── directives/         # Custom Vue directives (v-safe-html for DOMPurify)
├── locales/            # i18n YAML files (23 languages)
├── mixins/             # Vue mixins (StateMixin, FilesMixin, etc.)
├── monaco/             # Monarch tokenizers and editor themes
├── plugins/            # Vue plugins (i18n, socketClient, vuetify, filters, colorSet)
├── router/             # Vue Router (hash mode) — no route-level auth guards
├── scss/               # Global styles and Vuetify variable overrides
├── store/              # 28 Vuex modules (printer, files, config, webcams, etc.)
├── types/              # UI-specific TypeScript types
├── typings/            # Global .d.ts declarations (Klipper, Moonraker namespaces)
├── util/               # Helper functions (30+)
├── views/              # Page components (Dashboard, Console, Jobs, etc.)
└── workers/            # Web Workers (parseGcode, mjpegStream, sandboxedEval, Monaco language providers)

Router & Authentication

  • Hash-based routing (#/path)
  • Views lazy-loaded via dynamic imports: component: () => import('@/views/X.vue')
  • No route-level auth guard — authentication is handled entirely by App.vue: socketReady → render main app; socketAuthenticating → render Login overlay; else → render SocketDisconnected. Navigating to /login is redirected to home (the route no longer exists)
  • Socket state machine (src/store/socket/actions.ts): initializing → {connecting | disconnected} → identifying → {ready | authenticating}. initializing is the one-shot startup state; the app begins here and leaves it once $socket.connect() runs — transitioning to connecting (valid URL) or disconnected (empty URL). Every transition goes through socket/onSetStatus, which validates the edge against VALID_TRANSITIONS, commits the new status, and runs side-effects for the destination state. Entering connecting clears per-socket identity (connectionId, acceptNotifications); ready → connecting additionally resets modules holding live data (charts + MODULES_TO_RESET_ON_DROP). Re-running appInit (instance switch) calls store.dispatch('reset') which mutates socket state back to initializing directly via setReset, bypassing the state machine
  • JWT auth over WebSocket: runIdentify sends server.connection.identify with the stored user token (refreshed proactively if expired); if both tokens are expired, identify is called without a token (anonymous/trusted identify). On success it awaits the post-auth bootstrap — every Moonraker DB namespace Fluidd owns (skipping any with empty ROOTS), then server.info, server.config, machine.proc_stats, machine.system_info, and server.files.list('config') — before transitioning to ready; on failure → authenticating. server.connection.identify is one-shot per socket, so the logout→login path (same physical socket, new user via access.login) skips the identify call (connectionId already set) and runs only the bootstrap before transitioning to ready. The ready transition itself has no side-effects — entering ready simply unblocks the main app render
  • auth/login stores the fresh tokens then dispatches socket/onSetStatus with identifying to re-identify the live socket. auth/logout (full) transitions the socket to authenticating; the socket stays open and App.vue renders the Login overlay while socket.status === 'authenticating', so login continues over the existing connection via access.info / access.login. notifyUserLoggedOut (fired when Moonraker invalidates the session) does the same
  • Token refresh policy (getAccessToken in src/store/socket/actions.ts): valid access token → use it; expired access token + valid refresh token → call access.refresh_jwt and use the new token; refresh rejected with code 401 (or both tokens unusable) → clear both from localStorage and identify anonymously; refresh rejected with anything else (transient: socket drop, network) → keep tokens, return null, let the next identify cycle retry
  • Key routes: /, /console, /jobs, /tune, /diagnostics, /timelapse, /history, /system, /configure, /settings, /camera/:cameraId, /preview

Icons & Theming

  • MDI icons via @mdi/js — mapped in src/globals.ts (Icons object, ~228 mappings)
  • Usage: <v-icon>{{ $globals.Icons.close }}</v-icon>
  • Vuetify theme with custom dark/light overrides in src/scss/variables.scss
  • PWA support with service worker in src/sw.ts (Workbox, injectManifest strategy)

Monaco Editor

  • Setup in src/components/widgets/filesystem/setupMonaco.ts (includes worker environment setup)
  • Monarch tokenizers for gcode, klipper-config, moonraker-config, log languages (in src/monaco/language/*.monarch.ts)
  • Custom CodeLens providers (links to Klipper/Moonraker docs from config sections)
  • CodeLens and document symbol providers for klipper-config and moonraker-config; folding range provider for klipper-config, moonraker-config, and gcode
  • Language providers run in dedicated Web Workers (monacoCodeLensWorker, monacoDocumentSymbolsWorker, monacoFoldingRangesWorker)

Integration Points

Klipper/Moonraker Communication

  • All printer commands via SocketActions methods — both init and live data flow over a single WebSocket
  • Store updates from WebSocket events (not polling)
  • File operations through Moonraker's file API (src/store/files/)
  • File uploads/downloads are the sole consumer of axios — direct calls in src/mixins/files.ts (for upload/download progress, which fetch cannot report for uploads), authenticated with a oneshot token fetched via SocketActions.accessOneshotToken(). There is no HTTP client plugin, and the rest of the app uses fetch or the WebSocket

Component Communication

  • Parent-child: Props down, events up
  • Cross-component: Vuex store or EventBus (src/eventBus.ts)
  • Flash messages: EventBus.$emit(text, { timeout }) — displayed by FlashMessage component

Dynamic Imports

  • import.meta.glob() used in src/dynamicImports.ts for lazy-loading:
    • I18nLocales — locale YAML files
    • CameraComponents — camera service Vue components
  • Views also dynamically imported in src/router/index.ts via () => import('@/views/X.vue')

Testing Conventions

  • Unit tests in src/util/__tests__/*.spec.ts with Vitest + jsdom
  • Monarch tokenizer tests co-located in src/monaco/language/__tests__/ — use shared tokenize-helper.ts (registerLanguage, tokenizeLines, tokenBuilder)
  • Global test functions (describe, it, expect) — globals: true in vitest config
  • Setup file: tests/unit/setup.ts — includes CSS.escape and window.matchMedia polyfills required by Monaco in jsdom
  • Time manipulation utility: timeTravel(date, callback) in tests/unit/utils.ts
  • Parameterized tests: it.each([...]) pattern
  • Test store actions/mutations independently from UI

Code Style

  • Source must pass linting with zero warnings and zero type errors — run npm run lint and npm run type-check before committing

  • Vue class-style components with vue-property-decorator (@Component, @Prop, @VModel, Mixins())

  • ESLint enforced: neostandard + pluginVue.configs['flat/vue2-recommended'] + pluginRegexp + @vue/eslint-config-typescript

  • .editorconfig rules: 2 spaces, LF line endings, UTF-8, trim trailing whitespace, max line 100 (code)

  • camelCase for variables/methods, PascalCase for components

  • Use consola for logging, not console.log (configured in src/setupConsola.ts — warn in prod, verbose in dev)

  • Type imports: import type { ... } for types only (verbatimModuleSyntax: true)

  • satisfies keyword for store module type checking

  • No double-cast type assertions (as unknown as T) — use a proper TypeScript type guard instead (in, typeof, instanceof, or a custom type predicate):

    // Bad
    const x = value as unknown as { test: () => void }
    x?.test?.()
    
    // Good
    if (value && 'test' in value && typeof value.test === 'function') {
      value.test()
    }

Git & Contribution Policy

  • Conventional commits required: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, types, i18n
  • Commit subject max 50 characters — hard-enforced by .husky/commit-msg hook
  • Signed-off-by line required on all commits (use git commit -s): Signed-off-by: Your Name <your@email>
  • PR titles must follow conventional commits — CI-enforced via amannn/action-semantic-pull-request (scope optional)
  • PR branches must be off a branch other than develop or master
  • Clean develop preferred: squash and rebase feature branches prior to merge
  • CHANGELOG visibility: only feat, fix, perf, refactor appear in CHANGELOG.md (configured in .versionrc.json)
  • CI pipeline order: npm cilint --no-fixtype-checktest:unitcircular-checkbuild

Common Gotchas

  • Vue 2.7 limitations: no Composition API in production builds
  • WebSocket reconnection handled automatically by socketClient.ts
  • File uploads use FormData with progress tracking in store
  • Dynamic imports for code splitting (see vue-echarts-chunk.ts, src/dynamicImports.ts)
  • SCSS deprecation warnings silenced: import, global-builtin, slash-div, if-function
  • @/scss/variables auto-injected into all SCSS/Sass files via Vite config
  • path aliased to path-browserify for browser compatibility
  • Strict Vuex mode enabled only in dev (strict: import.meta.env.DEV)
  • SVG files auto-optimized on commit — pre-commit hook runs SVGO on staged .svg, .vue, and src/globals.ts files
  • VUE_ env prefix required — only env vars prefixed VUE_ are exposed to app code via import.meta.env (Vite envPrefix)
  • import.meta.env.VERSION and import.meta.env.HASH (short git hash) are injected at build time
  • server/config.json is the runtime config source (deployed as dist/config.json) — contains theme presets, endpoints, hosted flag
  • Translations managed via Weblate — do not directly edit non-English locale files in src/locales/

Dev Container

  • VSCode Dev Container (.devcontainer/) bundles a docker-klipper-simulavr container — real Klipper/Moonraker simulation on port 7125, Fluidd on port 8080
  • postCreateCommand runs npm ci && npm run bootstrap automatically

Documentation Site

  • Zensical (Material for MkDocs successor) — static site generator in docs/
  • Config: docs/zensical.toml — nav, theme, extensions, social links
  • Content: docs/docs/ — Markdown files with YAML frontmatter
  • Overrides: docs/overrides/ — custom Jinja2 templates (header, htmltitle)
  • Custom CSS: docs/docs/stylesheets/extra.css — Fluidd brand colors
  • Glossary: docs/includes/glossary.md — abbreviation tooltips auto-appended to all pages
  • Lint: markdownlint --config docs/.markdownlint.json docs/docs/
  • Install: cd docs && python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
  • Build: cd docs && zensical build --clean
  • Serve: cd docs && zensical serve or npm run serve:docs (localhost:8000)
  • Deploy: GitHub Actions (.github/workflows/docs.yml) — builds on push to master, deploys to gh-pages with docs.fluidd.xyz CNAME

Documentation Structure

docs/
├── docs/                  # Markdown content
│   ├── index.md           # Homepage
│   ├── getting-started.md # Installation (KIAUH, Docker, Manual, fluidd.xyz, FluiddPI)
│   ├── configuration.md   # Fluidd Config, Klipper, Moonraker, Multiple Printers
│   ├── customize.md       # Layout, themes, hiding components
│   ├── features/
│   │   ├── index.md           # Features overview (section landing page)
│   │   ├── authorization.md
│   │   ├── cameras.md
│   │   ├── console.md
│   │   ├── diagnostics.md
│   │   ├── file-editor.md     # Monaco editor features, syntax, CodeLens, folding
│   │   ├── file-manager.md    # File browser, upload, search, previews, drag-and-drop
│   │   ├── job-queue.md       # Sequential printing queue
│   │   ├── keyboard-shortcuts.md  # Global, editor, console keyboard shortcuts
│   │   ├── localization.md
│   │   ├── macros.md
│   │   ├── multi-material.md  # Multiple extruders + Spoolman
│   │   ├── multiple-printers.md
│   │   ├── printing.md        # G-code viewer, thumbnails, bed mesh, print history
│   │   ├── slicer-uploads.md
│   │   ├── system-and-notifications.md  # System info + notifications
│   │   ├── thermals.md        # Chart, presets, sensors
│   │   ├── third-party-integrations.md  # Kalico, Happy Hare, AFC, Beacon, Obico, OctoEverywhere, etc.
│   │   ├── timelapse.md
│   │   └── updates.md
│   ├── development.md     # Dev container, local dev, localization
│   ├── faq.md             # Organized by topic (Setup, Cameras, System, Printing)
│   └── sponsors.md
├── includes/
│   └── glossary.md        # Abbreviation definitions (auto-appended)
├── overrides/             # Jinja2 template overrides
├── zensical.toml          # Site configuration
└── .markdownlint.json     # Lint rules (MD013 and MD025 disabled)

Documentation Conventions

  • Frontmatter: title (required), icon (top-level pages only, Lucide icons)
  • Images: /assets/images/ path, stored in docs/docs/assets/images/
  • Code blocks must always have a language tag: ini for Klipper/Moonraker config, bash for shell commands, json for JSON, text when no language applies
  • Zensical uses Python-Markdown which requires 4-space indentation per nesting level for all block-level elements nested in lists (sub-lists, paragraphs, code blocks, blockquotes) — no tabs
  • Tables must use aligned pipe style (columns padded to equal width)
  • Links: use {.md-button} attribute for standalone action links
  • Keys: use ++key++ syntax (pymdownx.keys extension) instead of <kbd>
  • Terminology: G-code (not gcode/Gcode), Wi-Fi (not WiFi), GitHub (not Github), Node.js (not NodeJS), SD card (not SDCard), em dash (—) not hyphen (-) for parenthetical dashes
  • Klipper macro names: format as inline code (e.g., PAUSE, SET_PAUSE_AT_LAYER, _CLIENT_VARIABLE)
  • Klipper/Moonraker section names and config variable names: format as inline code (e.g., [virtual_sdcard], enable_object_processing) — exception: leave unformatted when used as markdown headings
  • Glossary terms (AFC, API, CNC, CORS, JWT, MCU, MMU, MPC, PID, etc.) get automatic tooltips via docs/includes/glossary.md
  • When introducing acronyms in docs, check if they exist in the glossary — if not, assess whether they should be added (domain-specific or non-obvious acronyms: yes; universally known ones like USB, HTTP, CPU: no)
  • Before committing docs changes, always run:
    • markdownlint --config docs/.markdownlint.json docs/docs/ — must be clean
    • codespell docs/docs/ — must be clean

Communication Style

  • Be extremely concise in responses
  • Sacrifice grammar for brevity
  • Focus on essential info only