Chat walkthrough

NETSYSTEM CMS · AI ARCHITECTURE DEEP DIVE ns_ai_chat · ns-ai-daemon · agy · CDP · items AI Section #100021

How ns_ai_chat & the NetSystem AI Agent Architecture Work

A complete, end-to-end technical walkthrough of the NetSystem CMS AI subsystem: from the frontend SSE chat client and PHP session proxy, through the multi-threaded Unix-socket daemon (ns_ai_daemon.py) and agy CLI subprocess, to dynamic prompt synthesis, per-stream headless Chrome CDP isolation (browser.py), authenticated CMS action dispatch (cms_call.py), and the declarative/imperative plugin lifecycle of the items module.

4 Layers
Browser JS → PHP Proxy → Python Daemon → agy CLI + MCP
Unix Socket
/var/run/ns-ai-daemon/ns-ai-daemon.sock (0660 root:apache)
CDP Port 9222
Per-stream isolated BrowserContext + 300s idle reaper
15+ Plugins
Declarative ai_field & custom items CRUD hooks
SECTION 01

End-to-End Architecture & Interactive Component Map

Click any node in the architecture diagram below to inspect its exact source files, runtime process owner, communication protocol, environment variables, and responsibilities.

1. USER BROWSER 2. APACHE / PHP CMS (apache) 3. AI DAEMON & AGENT SANDBOX (user: ns-ai-daemon) SSE / POST Conversations & Steps Unix Socket :0660 stream-json createBrowserContext POST /action/<id>/<act> CDP :9222 (WS) HTTPS + Cookie ns_ai_chat.js ReadableStream SSE Parser Attachments · Resume · Steps CMS Session Cookies HttpOnly Auth State token · permpin · PHPSESSID ns_ai_chat.php + libmod SSE Proxy & Context Builder ns_ai_build_context() MySQL Database ns_ai_conversations · messages sitemap · htmlpages · items CMS Target Modules inc/layout_func.php Dispatch items · treemenu · sitelook htmlbox · siteconfig · p_* check_crosssite_action() ns_ai_daemon.py ThreadingUnixStreamServer ActiveStream · Ring Buffer agy CLI Agent --output-format stream-json brain/<agy_conv_id>/ cms_call.py Authenticated Action CLI Cookie: $CMS_COOKIE browser.py + MCP Storage.setCookies + DevTools status · auth · clear ChromeManager (Port 9222) /opt/chrome-for-testing/chrome Per-Stream Incognito Context CMS_BROWSER_CONTEXT_ID 300s Idle Auto-Reaper
LAYER 1 · FRONTEND CLIENT

ns_ai_chat.js & ns_ai_chat.php

Files: modules/javascript/ns_ai_chat.js, modules/css/ns_ai_chat.css Protocol: fetch() + ReadableStream SSE consumer

The browser UI manages multi-conversation threads, file attachments (drag-drop, clipboard paste, or file picker up to 5 files / 10 MB each), live Markdown rendering, and collapsible agent reasoning/tool-call steps.

  • Stream Resumption: If a user reloads the page while the agent is still working, chat_history returns active_stream_id and the client automatically calls chat_resume to replay all buffered SSE chunks from offset 0 and continue streaming live.
  • Action URLs: Generated in PHP with $this->mode = 'default' before calling $this->getURLAction() so plain HTTP URLs are placed in data-* attributes.
SECTION 02

How the ns_ai_chat Module Works (Frontend, PHP & Database)

ns_ai_chat is a standalone CMS module that bridges authenticated administrators/creators in the browser to the local AI daemon while persisting conversation history, tool traces, and attachments in MySQL.

1. Module Registration & Options (modules/registry/ns_ai_chat_reg.php)

The module registers runtime options stored in sitemap.options and exposes one AI action (share_file) back to the agent:

Option Type / Hook Default Purpose
minshowlevel input 10 Minimum user privilege level required to view/use the chat UI ($this->canSee()).
mineditlevel input 10 Minimum level for editing module settings or invoking share_file ($this->canEdit()).
preset select "" Either "" (General Assistant) or "site_design" (Site Design & Content Assistant).
system_prompt textarea "" Optional custom instructions appended to the system context for this chat instance.
target_modules textarea (_var_export / _eval) array() List of module instance IDs (from sitemap.id) that this chat instance is allowed to inspect and control.
Clone Sharing via $this->realmoduleid: On test page section=21316, module #21319 is a clown (clone) of ns_ai_chat module #15425 (on section=15422). Because ns_ai_chat.php keys all database queries in ns_ai_conversations by (int)$this->realmoduleid (15425) and (int)$_SESSION['id'], conversations are seamlessly shared across cloned instances while still passing the current page's $this->section (21316) to ns_ai_build_context()!

2. Database Schema (ns_ai_conversations & ns_ai_messages)

Two dedicated MySQL tables store chat state, multi-turn agy session bindings, intermediate tool steps, and inline image previews:

CREATE TABLE `ns_ai_conversations` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `moduleid` int(11) NOT NULL DEFAULT 0,          -- $this->realmoduleid (e.g. 15425)
  `userid` int(11) NOT NULL DEFAULT 0,            -- $_SESSION['id']
  `agy_conversation_id` varchar(128) DEFAULT NULL,-- UUID from agy "init" event
  `title` varchar(255) NOT NULL DEFAULT '',       -- Auto-derived from first prompt
  `created_at` datetime NOT NULL DEFAULT current_timestamp(),
  `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  `active_stream_id` varchar(64) DEFAULT NULL,    -- 32-char hex while streaming, NULL when done
  PRIMARY KEY (`id`),
  KEY `module_user` (`moduleid`, `userid`)
);

CREATE TABLE `ns_ai_messages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `conversation_id` int(11) NOT NULL,             -- FK to ns_ai_conversations.id
  `role` varchar(32) NOT NULL DEFAULT 'user',     -- 'user' | 'assistant'
  `content` mediumtext NOT NULL,                  -- Final Markdown response text
  `steps` mediumtext DEFAULT NULL,                -- JSON array of thought/tool/response steps
  `created_at` datetime NOT NULL DEFAULT current_timestamp(),
  `attachments` mediumtext DEFAULT NULL,          -- JSON array of attachment metadata + data_uri
  PRIMARY KEY (`id`),
  KEY `conversation_id` (`conversation_id`)
);

Why steps is stored as JSON: As the stream progresses, PHP accumulates an ordered $steps array of {type: "thought"|"tool_call"|"response"|"error"|"aborted", ...} objects in memory and writes the complete JSON array to ns_ai_messages.steps when the stream finishes. When reloading history via chat_history, the UI reconstructs every collapsible thought and tool-call card identically to how it looked live.

3. PHP Controller Actions in modules/ns_ai_chat.php & localib/libmod.php

POST chat_stream

Starts or continues a conversation turn and proxies the daemon's SSE stream to the browser:

  1. Checks $this->canSee() (returns HTTP 403 JSON if unauthorized).
  2. Processes $_FILES['attachments'] (up to 5 files, max 10 MB each): base64-encodes file bytes for the daemon payload, and for images ≤ 2 MB stores a data:image/...;base64,... URI in $dbAttachments.
  3. Creates or fetches the row in ns_ai_conversations and inserts the 'user' message row into ns_ai_messages.
  4. Generates $streamId = bin2hex(random_bytes(16)) and stores it in ns_ai_conversations.active_stream_id.
  5. Calls ns_ai_build_context($this) to build the dynamic system prompt.
  6. Calls session_write_close() and ignore_user_abort(true) so the PHP session lock is released before streaming begins (allowing concurrent chat_abort or agent callbacks using the same PHPSESSID) and so PHP continues saving the final message to MySQL even if the user closes their browser tab!
  7. Sends event: conversation and invokes ns_ai_stream_daemon('/v1/chat/stream', $daemonPayload, $onEvent).
POST/GET chat_resume & chat_history

Provides seamless mid-stream page reload recovery:

  1. When a conversation is clicked, chat_history returns all saved messages plus active_stream_id from ns_ai_conversations.
  2. If active_stream_id is non-empty, ns_ai_chat.js immediately connects to chat_resume with stream_id and offset=0.
  3. chat_resume calls ns_ai_stream_daemon('/v1/chat/resume', ['stream_id' => $streamId, 'offset' => $offset]), which replays all buffered SSE events from the daemon's memory ring-buffer and seamlessly transitions to live streaming.
  4. Note: In chat_resume, PHP only forwards SSE events to the browser; the original chat_stream PHP worker (kept alive via ignore_user_abort(true)) remains solely responsible for persisting the final assistant row to ns_ai_messages so it is never saved twice!
POST chat_abort & chat_delete

Lifecycle termination and storage cleanup:

  1. chat_abort clears active_stream_id = NULL in MySQL and sends POST /v1/chat/abort to the daemon socket, which kills the agy process group (SIGTERM → SIGKILL) and disposes its headless Chrome BrowserContext.
  2. chat_delete aborts any running stream, sends POST /v1/conversation/delete to purge /var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/<agy_conversation_id>, and deletes the conversation & messages from MySQL.
POST share_file (AI Action)

Allows the AI agent to publish files directly into the chat response:

  1. Invoked by the agent via cms_call.py <domain> <chat_mod_id> share_file filename=chart.png content_base64=....
  2. Validates filename extension against dangerous executable extensions (php, phtml, phar, sh, py, pl, cgi, htaccess) and caps size at 20 MB.
  3. Writes the decoded file to /cache/files/ai_shared/<stream_id>/<safe_name> and returns JSON with url and ready-to-paste markdown (![name](url) for images, [name](url) for documents).
SECTION 03

The Background Daemon Behind It (/usr/local/bin/ns_ai_daemon.py)

Because PHP-FPM workers are request-scoped and run under apache, NetSystem CMS delegates agent execution and headless browser lifecycle management to a dedicated, unprivileged Python systemd service communicating over a Unix domain socket.

1. Systemd Service & Unix Socket Security

Defined in /etc/systemd/system/ns-ai-daemon.service:

[Service]
Type=simple
User=ns-ai-daemon
Group=ns-ai-daemon
WorkingDirectory=/var/lib/ns-ai-daemon
Environment="HOME=/var/lib/ns-ai-daemon"
Environment="PATH=/var/lib/ns-ai-daemon/.local/bin:/usr/local/bin:/usr/bin:/bin"
RuntimeDirectory=ns-ai-daemon
RuntimeDirectoryMode=0750
ExecStart=/usr/bin/python3 /usr/local/bin/ns_ai_daemon.py
ExecStartPost=/bin/sh -c 'for i in 1 2 3 4 5; do [ -S /var/run/ns-ai-daemon/ns-ai-daemon.sock ] && /bin/chgrp apache /var/run/ns-ai-daemon/ns-ai-daemon.sock && exit 0; sleep 1; done; exit 1'
  • Socket Permissions: ns_ai_daemon.py binds /var/run/ns-ai-daemon/ns-ai-daemon.sock and sets os.chmod(SOCKET_PATH, 0o660), while ExecStartPost sets its group to apache. Only root, ns-ai-daemon, and apache (PHP) can connect.
  • Process Isolation: The agent runs as user ns-ai-daemon, so it has zero direct filesystem write access to /vhosts/*/httpdocs or the MySQL database. All CMS reads/writes must go through authenticated HTTP endpoints via cms_call.py!

2. ActiveStream Ring Buffer & Pub/Sub Architecture

When POST /v1/chat/stream arrives, the daemon creates an ActiveStream(stream_id) instance in ACTIVE_STREAMS and spawns a background thread running _run_stream(stream, req):

class ActiveStream:
    def __init__(self, stream_id):
        self.stream_id = stream_id
        self.events = []              # Append-only list of (event_name, data_dict)
        self.cond = threading.Condition()
        self.done = False
        self.aborted = False
        self.proc = None              # subprocess.Popen handle for agy

    def publish(self, event_name, data_dict):
        with self.cond:
            self.events.append((event_name, data_dict))
            self.cond.notify_all()

    def subscribe(self, offset=0):
        idx = max(0, int(offset))
        while True:
            with self.cond:
                while idx >= len(self.events) and not self.done:
                    self.cond.wait(timeout=15.0)
                    if idx >= len(self.events) and not self.done:
                        yield None    # Emits ": heartbeat\
\
" every 15s
                while idx < len(self.events):
                    ev = self.events[idx]; idx += 1
                    yield ev
                if self.done and idx >= len(self.events):
                    break

Once stream.finish() is called, a delayed cleanup timer keeps the completed ActiveStream in ACTIVE_STREAMS for 60 seconds so late-reconnecting clients can still read the final events.

3. Daemon Filesystem Layout & Automated Storage Pruning

/var/lib/ns-ai-daemon/
├── cms_call.py                 # CLI helper for CMS AI actions
├── browser.py                  # CLI helper for CDP cookie auth & status
├── .local/bin/agy              # Antigravity CLI binary
├── .gemini/
│   ├── config/mcp_config.json  # Registers chrome-devtools MCP on :9222
│   └── antigravity-cli/brain/  # Multi-turn conversation trajectories
│       └── <agy_conv_id>/
│           └── .system_generated/logs/
│               ├── transcript.jsonl
│               └── transcript_full.jsonl
└── projects/
    └── managedev.netsystem.gr/ # CWD for agy on this domain
        ├── uploads/<stream_id>/# Materialized user file attachments
        └── tmp/<stream_id>/    # Agent scratch space for downloads/scripts

Background Reaper Thread (_cleanup_loop)

Runs every 15 minutes (CLEANUP_INTERVAL_SECONDS = 900) to prevent disk exhaustion:

  • Session Directories (prune_session_dirs): Deletes projects/<domain>/tmp/<stream_id> and uploads/<stream_id> directories older than 1 hour (3600s), skipping any stream_id currently in ACTIVE_STREAMS.
  • Brain Conversations Age Limit (prune_brains): Deletes any brain/<conv_id> directory whose mtime is older than 30 days.
  • Brain Total Size Cap: If total size of brain/ exceeds 2 GiB, evicts the oldest conversations by mtime until usage drops below 2 GiB.
SECTION 04

The agy Invocation & Interactive SSE Stream Simulator

Inside _run_stream(), ns_ai_daemon.py prepares an isolated Chrome context, materializes attachments, constructs the full prompt, spawns agy with --output-format stream-json, and translates every JSONL line emitted on stdout into Server-Sent Events (SSE).

Exact agy Command Line & Subprocess Environment

# Executed by /usr/local/bin/ns_ai_daemon.py in cwd=/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr
/var/lib/ns-ai-daemon/.local/bin/agy \\
  -p "<system_instructions>...</system_instructions>\
\
<attached_files>...</attached_files>\
\
<user_prompt>" \\
  --output-format stream-json \\
  --dangerously-skip-permissions \\
  --conversation "15c5b07d-87cc-4326-b1c2-4d44fd3af61f"   # Omitted on turn 1; passed on subsequent turns

# Environment variables injected into the agy process group (start_new_session=True):
HOME="/var/lib/ns-ai-daemon"
PATH="/var/lib/ns-ai-daemon/.local/bin:/usr/local/bin:/usr/bin:/bin"
CMS_COOKIE="PHPSESSID=...; token=...; permpin=..."        # Forwarded from PHP $_SERVER['HTTP_COOKIE']
CMS_STREAM_ID="7cd66de9c26b910e58bf40d1d440326a"          # 32-char hex stream ID
CMS_BROWSER_CONTEXT_ID="E891A42F..."                      # Ephemeral CDP BrowserContext ID
CMS_BROWSER_TARGET_ID="39A17B0C..."                       # Ephemeral CDP Page Target ID
INTERACTIVE VISUALIZATION

Live SSE & agy Stream-JSON Step-Through Simulator

Step through the 10 stages of a real ns_ai_chat execution turn to see how the Browser UI, PHP SSE Proxy, agy stdout JSONL, and MySQL tables stay synchronized.

A. What Happens in This Step
B. agy stdout (stream-json) / Internal Call

        
C. SSE Wire Frame & MySQL State

        
SECTION 05

Generated Prompts in Example Cases (ns_ai_build_context + Daemon)

Every turn sent to agy has its prompt synthesized in two stages: first by PHP's ns_ai_build_context($chatMod) in localib/libmod.php (which introspects live CMS state, permissions, presets, and target module registries), and then by Python's build_system_instructions() and process_attachments() in ns_ai_daemon.py.

How the Two-Stage Prompt Pipeline Works

Stage 1A · PHP Clean Page URL & Preset Context (localib/libmod.php:2213-2278)

Resolves the clean current page URL via getSiteBase() . str_replace('&amp;', '&', getURL(0)) (automatically stripping /action/<id>/stream_chat and tmpvars), active languages via get_active_langs('short'), and the preset's instructions, required_modules, and optional_modules.

Stage 1B · PHP Target Instance Introspection & htmlbox Scope Metadata (localib/libmod.php:2285-2320)

Resolves each module in target_modules (handling clown clones via $modinst->getModuleName()), falls back to $modinst->getHumanID() when no manual description is given, and automatically annotates every htmlbox with [scope: page-specific | global all-pages; layers: html, css, js, head].

Stage 2A · Daemon (Turn 1) Full System Instructions & Filtered Optional Modules (ns_ai_daemon.py:282-415)

Filters optional_modules against configured_module_names so only unconfigured modules are suggested, formats deduplicated ai_actions schemas, injects the isolated CDP targetId and pre-created tmp/<stream_id>/ directory, and appends process_attachments().

Stage 2B · Daemon (Turn 2+) Multi-Turn Delta Context Update (build_follow_up_instructions())

When resuming a conversation via --conversation <uuid>, agy already retains Turn 1 in its history. The daemon sends a compact 1.4 KB delta update (current section/URL, configured modules, new tmp/<stream_id>/, and new CDP targetId), cutting follow-up prompt overhead by 88.4%.

INTERACTIVE PROMPT EXPLORER

Inspect the Exact Generated Full Prompt Across 5 Real Scenarios

Select any scenario below to view the verbatim prompt passed to agy -p <full_prompt>. All panels support browser Find-in-Page (Ctrl+F / Cmd+F) via hidden="until-found".

Source: Captured verbatim from live transcript 65c78b25-5b28-48fe-9819-db7b6e57a1d1 on section=21316 (module #21319, clone of #15425) Preset: site_design Size: 9.8 KB (Turn 1 full prompt)
You are the NetSystem CMS Assistant for domain: managedev.netsystem.gr.
You manage content and settings for the modules on this website.
You have access to the local management helper `cms_call.py` and `browser.py` (execute via your `run_command` tool).
For inspecting or modifying `managedev.netsystem.gr`, ALWAYS use `cms_call.py` and Chrome DevTools browser tools — do NOT scrape `managedev.netsystem.gr` with `curl`.
STRICT SANDBOX RULE: NEVER read, grep, or write to `/vhosts/` or any server filesystem path outside your current working directory and `/var/lib/ns-ai-daemon/{cms_call.py,browser.py}`.
Current page / section ID: 21316
Current page URL: https://managedev.netsystem.gr/?section=21316&language=el_GR
Current language: el
Languages available: el (Ελληνικά), en (Αγγλικά)

--- Specialized Preset Instructions ---
### Role: NetSystem CMS Site Design & Styling Specialist
You are an expert site designer and front-end architect for websites built on NetSystem CMS.
Your goal is to help design, style, customize, and refine the visual appearance and content structure of this site.

### Architecture & Strict Module Responsibilities:
1. **Site Appearance & CSS Variables (`sitelook`) [REQUIRED]**:
   - ALL global site appearance settings, brand colors, typography, spacing, layout templates, and page classes MUST be managed through the `sitelook` module.
   - Always inspect and modify global settings (omit `section` or pass `section: 0`). Section-specific styling overrides should almost NEVER be used unless the user explicitly asks to style only one page differently.
   - Use `get_settings` to inspect global layout, default class, ungrouped settings, and `available_groups`, then `get_settings group=<group_name>` (e.g. `group=topmenu`, `group=topbanner`, `group=content`) for group variables.
   - Use `set_var` to update editable theme variables (e.g. `hlbasecolor`, font sizes). `metavars` (e.g. `hlcolor`, `hlsoftcolor`) are dynamically computed and read-only in `set_var`, but you SHOULD reference them in custom CSS (`var(--hlcolor)`, `var(--hlsoftcolor)`).
   - Use `get_classes`, `set_layout`, or `set_class` when adjusting layouts or page class templates.
   - **STRICT RULE**: If `sitelook` is not configured in `target_modules`, you MUST NOT write CSS overrides into `htmlbox` to change global theme variables/colors/layouts; ask the user to add the `sitelook` module ID to `target_modules`.

2. **Custom Component CSS & JavaScript (`htmlbox`) [OPTIONAL]**:
   - **Per-Page vs. Global `htmlbox` Selection**: Check each `htmlbox` instance's `[scope: ...; layers: ...]` annotation in the configured modules list:
     - For **page-specific** content, component CSS, or JS (affecting only the page you are creating/editing), ALWAYS pass `content`, `css`, and `js` to the **page-specific** `htmlbox` (`scope: page-specific`, with `section=<page_section_id>`) via `set_content`.
     - Use a **global** `htmlbox` (`scope: global all-pages`) ONLY for site-wide HTML/CSS/JS that must appear on every page of the site.
   - Inspect an `htmlbox` via `get_content` (returns `content`, `container_class` `.htmlbox<id>`, `css`, `js`, `head`) and update any combination of layers via `set_content`.
   - **MANDATORY SCOPING RULE**: When writing CSS rules for an `htmlbox`, ALWAYS scope your selectors using its container class `.htmlbox<id>` (e.g. `.htmlbox<id> .my-btn { ... }` or `.htmlbox<id> { ... }`). NEVER use `#id` and NEVER use unscoped global tag selectors.
   - Reference theme CSS variables (`var(--hlcolor)`, `var(--hlsoftcolor)`) instead of hardcoding brand colors.

3. **Navigation, Page Types (`class`), Structured Content & Site Metadata (`treemenu`, `items`, `siteconfig`) [OPTIONAL]**:
   - Use `treemenu` (`get_tree`, `get_node`, `add_node`, `update_node`, `move_node`, `delete_node`, `duplicate_node`) to inspect or build navigation pages. When creating a new page via `add_node`, note the returned `id` (the new page's `section` ID) and pass `section=<new_id>` when populating its page-specific `htmlbox`.
   - **How Page Types (`class`) & Module Rendering Work**:
     - Every page (`section`) has a **Page Type (`class`)** and a **Layout (`layout`, typically `default` or `onecol`)**.
     - Modules (such as a page-specific `htmlbox`, `gallery`, `items`, `forms`, or page `title`) are attached to a Page Type (`class`), and classes inherit from parent classes. When rendering a page, the CMS walks the page's `class` inheritance chain and renders only the modules belonging to that class chain (plus any modules attached directly to the page node itself).
     - Consequently, **a page-specific `htmlbox` (e.g. `htmlbox #341`) will ONLY render on a page if that page's `class` includes that `htmlbox`!** If a page uses a bare skeleton class (such as `771` *Μία στήλη* or `772` *Δύο στήλες*), `htmlbox #341` is not part of that class and will NOT appear on the page until the page's `class` is set (via `treemenu` `add_node` or `update_node`) to a content Page Type such as `524`.
     - Note: A `treemenu` instance may or may not be connected that gives access to all the classes. If such a `treemenu` is set up, you will be able to see every class and even create a class of your own (although creating a new class isn't really useful at the moment, as you cannot add modules on your own into that class).
   - **Common Page Types (`class`) & Typical Use** (inspect full list via `sitelook` `get_classes`):
     - **`524` — Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες**: Standard 2-column content page (left sidebar + main content). Includes the page title, the main page-specific `htmlbox` (`#341`, via clone `#746`), and photo galleries. **Default choice** for standard content pages, "About Us", services, or migrated/custom HTML/CSS/JS pages (pair with `layout=onecol` if you want a full-width 1-column presentation without the left sidebar).
     - **`1386` — Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες και φόρμα επικοινωνίας**: Inherits `524` (`htmlbox #341` + galleries) and adds a contact form. Typical use: Contact Us or inquiry pages.
     - **`1542` — Αρχική Σελίδα 2 στήλες**: Homepage class with homepage-specific banners and modules.
     - **`1519` — Σελίδα Νέων δύο στηλών**: News / articles / announcements listing page driven by the news `items` module.
     - **`1515` — Σελίδα Προϊόντων δύο στηλών**: Product catalog category page (2 columns, vertical list layout) driven by the products `items` module.
     - **`1517` — Σελίδα προϊόντων δύο στηλών με οριζόντια διάταξη**: Product catalog category page in a **grid** layout ("οριζόντια διάταξη" means in a grid, 2 columns) driven by the products `items` module.
     - **Base Skeleton Classes (`1` `baseclass` — every page, `49` `allpages` — all pages except the start page, `771` *Μία στήλη*, `772` *Δύο στήλες*)**: Internal structural base classes (`visible=0`) that only define shared site-wide modules or empty column containers and do **not** include `htmlbox #341` or page content modules. Do not assign these directly to content pages unless custom modules are attached directly to the page node.
   - **Note on `canhavecontent` in `treemenu`**: `canhavecontent` (`0`, `1`, `-1`) ONLY controls whether a **parent menu item that has child sub-pages** links to its own clickable page in the menu (*"Παρουσία περιεχομένου στη σελίδα όταν αυτή έχει υποσελίδες"*: typically we select `0` = use menu default; `1` = yes; `-1` = no, acts only as a submenu header). It has **no effect** on leaf pages or on whether `htmlbox` renders.
   - Use `items` (`describe` first, then `list_items`, `get_items`, `save_item`, `delete_items`) when working with structured catalog/article items.
   - Use `siteconfig` (`get_config`, `get_var`, `set_var`, `update_config`) for site-wide titles, logos, favicons, and metadata.

4. **Design & Inspection Standards**:
   - **Ignore CMS Admin UI**: When inspecting the page via Chrome DevTools as an authenticated user, `#adminbar` (and `.adminbar`, `.modedittitle`, module edit icons) is the CMS administration toolbar — it is NOT part of the public website design. NEVER target, restyle, or hide `#adminbar` unless explicitly asked.
   - Ensure clean visual hierarchy, modern typography, accessible color contrast (WCAG AA), and mobile-first responsive layouts.
--------------------------------------

Module instances available:
- treemenu, id 10, Main site menu
- sitelook, id 134, Sitelook module
- htmlbox, id 341, Κύριο περιεχόμενο σελίδας με τύπο κειμένου / φωτογραφιών [scope: page-specific; layers: html, css, js, head]
- htmlbox, id 15434, Custom CSS που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: css]
- htmlbox, id 15435, Custom JavaScript που παίζει σε όλες τις σελίδες [scope: global all-pages; layers: js]
- items, id 15305, Νέα - News

Proactive Setup Guidelines:
Unconfigured optional modules available for this preset: 'siteconfig' (Siteconfig module to manage site metadata, logos, favicons, and global configuration)
If you are implementing a request and determine that having an unconfigured module above would produce a cleaner, safer, or more standard solution, explain this to the user and suggest adding that module ID to target_modules.

treemenu module actions available:
- get_tree, Returns the hierarchical menu tree structure served by this treemenu instance as JSON...
- get_node, Returns full details and configuration of a specific menu node / page by ID...
- add_node, Adds a new menu node / page under the specified parent node in this menu. Parameters: ..., class (optional: Page Type (class) ID determining which modules render on the page (e.g. 524 for standard 2-column text/photos page with htmlbox)), ..., canhavecontent (optional: Only for parent nodes with child sub-pages: 0 (use menu default — typically selected), 1 (parent has its own clickable page), -1 (parent is only a submenu header). No effect on leaf pages), ...
- update_node, Updates properties of an existing menu node / page...
- delete_node, Deletes a menu node / page by ID...
- move_node, Moves/reorders a menu node up or down among siblings...
- duplicate_node, Duplicates/clones a menu node and its entire subtree...

sitelook module actions available:
- get_settings, Returns site appearance settings as JSON (global layout, default class, available_groups, ungrouped vars, and metavars when group is omitted; or only that group's variables). Parameters: group (optional: Optional group name (e.g. topmenu, topbanner, content)), section (optional: Section ID (omit or 0 for global appearance)).
- get_classes, Returns list of available page class templates with ID and hierarchical name.
- get_var, Returns the value of a specific theme / CSS variable. Parameters: name (required: Variable name), section (optional: Section ID (omit or 0 for global appearance)).
- set_var, Updates one or more editable theme / CSS variables (e.g. hlbasecolor) without altering layout or page class. Parameters: name (optional: Variable name), value (optional: New variable value), var[<name>] (optional: Variable map), section (optional: Section ID (omit or 0 for global appearance)).
...

htmlbox module actions available:
- get_content, Returns HTML content, container_class (htmlbox<id>), custom CSS stylesheet, JavaScript, and HEAD data of this htmlbox as JSON. Parameters: section (optional: Section ID (0 or omitted for global/all-pages htmlbox)), language (optional: Language code (e.g. "el", "en", or "nolang"; defaults to current language)).
- set_content, Updates HTML content, custom CSS styles, JavaScript, and/or HEAD data of this htmlbox. Content, css, js, and head can be provided together or independently. When writing component CSS, target the box wrapper using its scoped container class .htmlbox<id>. Parameters: content (optional: HTML content (string or JSON map by language; optional when updating css/js/head)), css (optional: Custom CSS stylesheet rules (scope selectors with .htmlbox<id>)), js (optional: Custom JavaScript executed on page load), head (optional: Custom raw HEAD markup (<meta>, <link>, external <script> tags)), section (optional: Section ID (0 or omitted for global/all-pages htmlbox)), language (optional: Language code (e.g. "el", "en", or "nolang"; defaults to current language)).
- htmlbox_change_notes, Updates internal admin notes for this htmlbox...

items module actions available:
- describe, Returns {module_id, title, can_edit, can_delete, fields:[{key, label, type, multilang, required, readonly, multiple?, values?, target?, filter?, group?, cssclass?, description?}], plugin_actions}. Parameters: section (optional: Category / Section ID scope), parent_item (optional: Parent item ID when nested (useparentitemidascat)).
- list_items, Returns {count, offset, has_more, field_labels?, items:[{id, name, url, ...fields}]}. Parameters: query (optional: Search keyword or #<id>), section (optional: Category / Section ID to filter by), parent_item (optional: Parent item ID when nested (useparentitemidascat)), filters (optional: JSON map of field_key => value or {"min": ..., "max": ...} range), limit (optional: Number of items to return (default: 20, max: 50, or 20 with fields)), offset (optional: Offset for pagination (default: 0)), fields (optional: Comma-separated field keys or * for all (default: summary only)), lang (optional: Language code (e.g. "el", "en")).
- get_items, Returns {field_labels?, items:[...]} with requested or all plugin fields (max 20 IDs). Parameters: ids (required: Comma-separated item IDs or JSON list (max 20)), fields (optional: Comma-separated field keys or * for all (default: *)), lang (optional: Language code (e.g. "el", "en")).
- save_item, Creates (omit id) or updates an item using exact field keys from describe (pass <field_key>=<value> args or fields=<json>). Omitted fields are preserved on update. Parameters: id (optional: Item ID to update (omit to create new item)), name (optional: Item name / title (string or JSON map by language)), section (optional: Category / Section ID for the item), parent_item (optional: Parent item ID when nested (useparentitemidascat)), fields (optional: JSON dictionary of field_key => value updates), lang (optional: Language code (e.g. "el", "en")).
- delete_items, Deletes specified items (max 20 IDs). Requires both edit permissions and the ai_allow_delete module option enabled. Parameters: ids (required: Comma-separated item IDs or JSON list (max 20)).

Configuration & Action Execution Instructions:
1. Scope (Global vs Section) & Page Types (`class`): Site-wide appearance and configuration settings (e.g. sitelook, siteconfig, global `htmlbox`) are global: omit `section` or pass `section=0` unless the user explicitly requests a section override. For page-specific `htmlbox` modules (`scope: page-specific`), pass `section=<page_section_id>`, and ensure that page's `class` (Page Type in `treemenu`) is a content class that includes that `htmlbox` (such as `524` 'Σελίδα Κειμένου/Φωτογραφιών με 2 στήλες', rather than bare skeleton classes like `771`/`772`).
2. Execute module actions via `run_command`:
python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <module_id> <action> [section=<section_id>] key=value ...
3. Multi-line File Payloads (`key=@filepath`): `cms_call.py` supports reading parameter values directly from a file by prefixing the path with `@` (e.g. `content=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/page.html`, `css=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/style.css`, `js=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/script.js`). For any multi-line HTML, CSS, or JS, save raw `.html`, `.css`, or `.js` files and pass `key=@filepath` — do NOT write Python `subprocess` wrapper scripts around `cms_call.py`, and on follow-up turns use `replace_file_content` to make targeted edits to those files instead of rewriting them from scratch.
Example (Global Settings): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <sitelook_module_id> set_var 'name=hlbasecolor' 'value=#0066cc'
Example (Page HTMLBox via @file or inline): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <htmlbox_module_id> set_content section=21316 content=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/page.html css=@tmp/st_7157f2db817b482b9d2b0af878f3e4f9/style.css
Example (Multilingual Content): python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr <htmlbox_module_id> set_content language=en 'content=<new_content>'
After running the command, explain what you updated and confirm to the user.

Browser Capabilities & Guidelines:
- Chrome DevTools MCP tools available (call directly — do NOT inspect MCP schema files on disk):
  * All page tools require an integer `pageId` parameter (use `pageId=1` for your isolated session tab, initially at `about:blank#st_7157f2db817b482b9d2b0af878f3e4f9` with `isolatedContext=...`; if multiple sessions are active, call `list_pages()` to check the `pageId` of your `isolatedContext` tab).
  * `list_pages()`, `navigate_page(pageId=1, url=...)`, `take_snapshot(pageId=1)`, `take_screenshot(pageId=1, filePath=...)`, `resize_page(pageId=1, width=..., height=...)`, `emulate(pageId=1, ...)`, `evaluate_script(pageId=1, function="() => { ... }")`, `click(pageId=1, uid=...)`, `fill(pageId=1, uid=..., value=...)`, `hover(pageId=1, uid=...)`, `press_key(pageId=1, key=...)`, `wait_for(pageId=1, text=...)`, `handle_dialog(pageId=1, action="accept"|"dismiss")`.
  * Combine DOM, content, and computed-style inspections into a single `evaluate_script` call that returns a compact structured object (headings, text paragraphs, image `src` and CSS `background-image` URLs, link `href`s, form inputs/options, computed styles). NEVER return full `document.body.innerHTML` or large `outerHTML` strings (>4 KB) from `evaluate_script`, as oversized outputs spill to disk files and get truncated.
  * When taking full-page screenshots (`take_screenshot(pageId=1, fullPage=true)`), headless Chrome does not scroll the page first, so below-the-fold `<img loading="lazy">` elements will remain unloaded unless you omit `loading="lazy"` or scroll the page before capturing.
- Responsive & Multi-Device Testing: Test across mobile (~390px), tablet (~800px), and desktop (~1440px) viewports, plus actual CSS breakpoints.
- Browser Authentication & Guest Mode:
  * Your isolated browser tab starts in unauthenticated guest mode.
  * To view pages or admin interfaces as the authenticated user, run: `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr auth` (injects `HttpOnly` session cookies invisible to `document.cookie`), then navigate or reload.
  * When authenticated, `#adminbar` is the CMS admin toolbar — ignore it when evaluating or styling public page layout.
  * To return to public guest mode: `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr clear` (or check status via `python3 /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr status`).

Workspace File Organization & Sharing:
- Dedicated Session Directory: `tmp/st_7157f2db817b482b9d2b0af878f3e4f9/` is ALREADY created for you (do NOT run `mkdir -p tmp/st_7157f2db817b482b9d2b0af878f3e4f9`).
- Store all screenshots and temporary files inside `tmp/st_7157f2db817b482b9d2b0af878f3e4f9/` (e.g. `take_screenshot(filePath="tmp/st_7157f2db817b482b9d2b0af878f3e4f9/screenshot.png")`). NEVER create loose files in the workspace root.
- To share a screenshot or file with the user, run:
  python3 /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 21319 share_file file=tmp/st_7157f2db817b482b9d2b0af878f3e4f9/<filename> [title="Description"]
- Copy the exact Markdown snippet returned by `share_file` directly into your response.

User Request: Ανέβασε αυτή την εικόνα σε ένα νέο άρθρο στα Νέα (#15305) με τίτλο "Νέα Εποχή 2026".
SECTION 06

How the Browser Functionality Works (ChromeManager, CDP & browser.py)

The agent can visually inspect live pages, verify responsive layouts, check console errors, and test both logged-in admin views and public visitor views using a shared Headless Chrome instance on port 9222 with strict per-stream incognito isolation.

1. ChromeManager & Per-Stream CDP Isolation

Implemented in /usr/local/bin/ns_ai_daemon.py (lines 58–207):

  • On-Demand Launch: When a chat stream starts, CHROME_MANAGER.create_isolated_context() checks if port 9222 responds to GET http://127.0.0.1:9222/json/version. If not, it launches /opt/chrome-for-testing/chrome --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 --window-size=1920,1080.
  • 300-Second Idle Reaper: A daemon thread (_reap_loop) checks every 15 seconds if get_active_stream_count() == 0 and time.time() - last_used > 300. After 5 minutes of idle time, Chrome is terminated with SIGTERM to free RAM.
  • Incognito Context Creation via Raw CDP WebSocket: Using a zero-dependency RFC 6455 WebSocket client (_ws_connect / _cdp_call), the daemon sends two browser-level CDP commands before agy starts:
    // 1. Create an isolated incognito BrowserContext (separate cookies/storage/cache)
    {"id": 1, "method": "Target.createBrowserContext", "params": {"disposeOnDetach": false}}
    // Returns: {"result": {"browserContextId": "E891A42F..."}}
    
    // 2. Create a dedicated 1920x1080 tab inside that BrowserContext
    {"id": 2, "method": "Target.createTarget", "params": {
      "url": "about:blank",
      "browserContextId": "E891A42F...",
      "width": 1920, "height": 1080
    }}
    // Returns: {"result": {"targetId": "39A17B0C..."}}
  • Guaranteed Teardown: In the finally: block of _run_stream(), the daemon calls Target.disposeBrowserContext({"browserContextId": browser_context_id}), immediately closing the tab and wiping all cookies/state for that stream.

2. Why browser.py Exists & How It Injects HttpOnly Cookies

In NetSystem CMS, authentication relies on three cookies: PHPSESSID, token, and permpin. Because token and permpin are marked HttpOnly, JavaScript running inside the page (document.cookie) cannot see them, nor can document.cookie = ... accurately set HttpOnly flags.

/var/lib/ns-ai-daemon/browser.py solves this by connecting directly to the page's CDP WebSocket (ws://127.0.0.1:9222/devtools/page/<CMS_BROWSER_TARGET_ID>) and executing privileged CDP Storage.* commands scoped to CMS_BROWSER_CONTEXT_ID:

Command CDP Method Used What It Does
browser.py <domain> status Storage.getCookies Checks if any cookie matching <domain> is in {"token", "permpin", "PHPSESSID"}. Outputs AUTHENTICATED (active cookies: PHPSESSID, permpin, token) or UNAUTHENTICATED.
browser.py <domain> auth Storage.setCookies Parses os.environ["CMS_COOKIE"] and injects every cookie into CMS_BROWSER_CONTEXT_ID with path="/", secure=True, and httpOnly=(name in {"token", "permpin"}).
browser.py <domain> clear Storage.getCookies + Network.deleteCookies Fetches all cookies in the isolated context, deletes cookies belonging to <domain>, and preserves any third-party cookies.
Multi-Session Safety Rule: Because multiple users/streams can use Headless Chrome on port 9222 simultaneously, each stream has its own CMS_BROWSER_TARGET_ID. The system prompt instructs the agent to always call the chrome-devtools MCP tool select_page(pageId=CMS_BROWSER_TARGET_ID) first and never call new_page or close_page.
SECTION 07

How the Connection to the CMS is Done from the Agent (cms_call.py)

Whenever the agent needs to read or mutate CMS state, it invokes /var/lib/ns-ai-daemon/cms_call.py via its run_command tool. Here is the exact request path from the CLI invocation to the PHP module method.

Step-by-Step Request Flow: cms_call.py → Apache .htaccess → index.php → inc/layout_func.php

1. /var/lib/ns-ai-daemon/cms_call.py

# Usage:
# cms_call.py <domain> <module_id> <action> [key=value ...]

url = f"https://{domain}/action/{module_id}/{action}"
body = urllib.parse.urlencode(params).encode("utf-8")

headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "User-Agent": "NetSystem-AI-Daemon/1.0",
}
cookie = os.environ.get("CMS_COOKIE", "").strip()
if cookie:
    headers["Cookie"] = cookie

req = urllib.request.Request(url, data=body, headers=headers, method="POST")

2. URL Rewrite & Kernel Action Dispatch

# 1. .htaccess rewrites /action/<id>/<action>:
RewriteRule ^action/([0-9]+)/([a-zA-Z0-9_-]+)$ \\
    index.php?tmpvars[$1][action]=$2 [QSA]

# 2. inc/layout_func.php (lines 88-115) dispatches the action:
$targetinst = getModule($db, $theid);
if (is_object($targetinst)) {
    if (method_exists($targetinst, $theaction)) {
        // Enforces HTTP method (ai_actions) & CSRF protection:
        check_crosssite_action($targetinst->getName(), $theaction);
        $targetinst->$theaction(); // Calls module method, which ends in $this->aiJSON(...) -> die()
    }
}
How check_crosssite_action() (localib/libmod.php:295-336) Protects AI Actions:
  1. HTTP Method Enforcement: Checks $modules_reg[$targetmodname]['ai_actions'][$theaction]['method']. All AI actions specify 'method' => 'POST'. If a request arrives via GET, it immediately responds with HTTP 405 Method Not Allowed: {"status":"error","error":"Method Not Allowed. Expected POST"}.
  2. Browser CSRF Protection: If the request comes from a browser with Sec-Fetch-Site: cross-site (or a mismatched Origin header) and the action is not in crosssite_actions, it aborts with HTTP 403 Forbidden. Server-to-server requests from cms_call.py (which carry the user's CMS_COOKIE and no cross-site browser headers) pass cleanly and inherit the exact $_SESSION['level'] of the user who initiated the chat.
SECTION 08

How CMS Modules Handle AI Calls (amodule Helpers & Core Modules)

Base class amodule in inc/base_func.php provides standardized input parsing and JSON response helpers so any CMS module can expose AI actions with minimal boilerplate.

1. Base Class AI Helpers in amodule (inc/base_func.php:789-862)

Method Signature & Behavior
$this->aiJSON($data, $code = 200) Sets http_response_code($code) and Content-Type: application/json; charset=utf-8. If $data is a string and $code >= 400, wraps it as ['status' => 'error', 'error' => $data]. Outputs json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) and calls die() to prevent page redirection.
$this->getAIPostData() Merges $_POST, raw JSON body from php://input (if $_POST is empty), and $this->localvars['tmpvars'] (excluding 'action') into a single associative array.
$this->getAIArray($val) Accepts either a PHP array or a JSON-encoded string and returns a decoded array (or array() on failure).
$this->parseIdList($raw) Accepts a JSON array ("[10, 20]"), comma-separated string ("10,20"), or array, filters for positive integers, and returns a deduplicated int[].
$this->aiLangValues($val, $lang = '') Normalizes a multilingual field input: if $val is a JSON object {"el":"...","en":"..."}, maps each language key; if it is a plain scalar string, assigns it to the target short language code (e.g. ['el' => $val]).
$this->aiBool($val) Normalizes truthy strings/ints ('1', 'true', 'yes', 'on') to true and falsy values ('', '0', 'false', 'no', 'off') to false.

2. treemenu (Navigation & Page Lifecycle)

Files: modules/treemenu.php & modules/registry/treemenu_reg.php

  • Scope Isolation (isNodeInMenu($id)): Every AI action verifies that the target node ID (or parent_id) lies within the subtree rooted at $this->getStartPage() (e.g. 734 on managedev.netsystem.gr). An agent given access to a sub-menu cannot touch pages outside that menu's subtree!
  • Multilingual Merging: In update_node, _parse_multilang_input($post['name']) is merged with existing serialized language names via array_merge($existingName, $nodeData['name']) so updating Greek does not erase English.
  • Actions: get_tree, get_node, add_node, update_node, delete_node, move_node, duplicate_node.

3. htmlbox (HTML, Scoped CSS, JS & HEAD)

Files: modules/htmlbox.php & modules/registry/htmlbox_reg.php

  • Storage Model: Stores rows in the htmlpages table keyed by (parent_id, section, language) where parent_id is the htmlbox module ID (e.g. 341), section is the page ID (or 0 when usesection=0, as on global CSS #15434 and global JS #15435), and language is:
    • 'el' / 'en' for HTML content (or '0' if uselang=0)
    • '_css' when allowcss=1 (compiled into the page's cached CSS bundle)
    • '_js' when allowjs=1 (appended via appendJS)
    • '_head' when allowhead=1 (injected into <head> via global_hook_html_head)
  • container_class: get_content and set_content return "container_class": "htmlbox341" so the agent always knows the exact wrapper class to scope its CSS rules under.

4. sitelook (Theme Variables, Layouts & Page Classes)

Files: modules/sitelook.php & modules/registry/sitelook_reg.php

  • Grouped Theme Inspection (get_settings): Without arguments, returns global layout, default class, available layouts, variable groups (topmenu, topbanner, content, etc.), ungrouped variables (with their CSS custom property names like var(--bodybackcolor)), and metavars. Passing group=topmenu returns all variables in that group.
  • Granular Updates: set_var updates individual CSS/theme variables without touching layout or page class, and triggers theme cache regeneration.

5. siteconfig (Global SEO, Header/Footer & robots.txt)

Files: modules/siteconfig.php & modules/registry/siteconfig_reg.php

  • get_config / set_config: Reads and updates global site settings for the target language (sitename, metadesc, metakeywords, header, footer, head, robots_txt, and base64 favicon).
SECTION 09

How the items Module Handles AI & What It Presents to the Agent

In NetSystem CMS, items is a composite container whose fields, prices, photos, dates, categories, and relationships are provided by child plugin modules (p_*). The items AI architecture turns any arbitrary combination of plugins into a self-describing, strongly validated JSON CRUD API.

1. The 5 AI Actions of items (modules/items.php:1580-2032)

describe — Dynamic Schema & Plugin Introspection (modules/items.php:1580-1688)

When the agent calls describe, items resolves its edit master ($editmaster = $this->getEditMaster()) and inspects every child plugin:

  1. Built-in Fields: If $editmaster->options['nameeditable'] is enabled, adds the name field (type: "text", multilang: true, required: true).
  2. Plugin Fields ($vinst->ai_fields()): Iterates $editmaster->getPlugins(false, true), escalates $vinst->level = $this->level, and calls $vinst->ai_fields() on each plugin. Each returned field descriptor includes: key (always $vinst->getInputName(), e.g. p_textbox-15298 or p_photos-15295), label, type, multilang, required, readonly, plus optional multiple, values, target, unit, filter, and description.
  3. Category / Parent Selectors: If sectioneditable is enabled, calls getEditablePages() and adds a section field with values: {sectionId: label} and filter: {"eq": "section"}. If parentitemeditable is enabled, adds parent_item.
  4. Embedded Plugin Actions (plugin_actions): If any child plugin in $this->getPlugins(false, true) is NOT an ai_fields provider (for example, an embedded htmlbox inside items) and has its own ai_actions in $modules_reg, describe exposes them in a plugin_actions array!

2. Zero-Boilerplate Declarative 'ai_field' vs. Custom Plugin Overrides

In inc/base_func.php (lines 864–1088), amodule implements default ai_fields(), ai_get($nodeid, $lang), ai_prepare($nodeid, $values, $lang), and ai_filter($filters) methods driven entirely by a declarative 'ai_field' array in modules/registry/p_*_reg.php!

Declarative Example: modules/registry/p_textbox_reg.php

'ai_field' => array(
    'type'              => array(
        'option' => 'type',
        'map'    => array('html' => 'html', 'text' => 'textarea'),
    ),
    'label'             => array('option' => 'label', 'default' => _('Κείμενο')),
    'multilang_option'  => 'uselang',
    'required_option'   => 'required',
    'post_format'       => 'lang_array',
),

With those 10 declarative lines in p_textbox_reg.php, modules/p_textbox.php requires zero PHP methods for AI support! amodule automatically reads $this->data_get($nodeid) in ai_get() and populates $_POST["p_textbox-15298"][$lang] in ai_prepare().

How post_format Maps Values into $_POST

  • 'lang_array' (p_textbox, p_inputbox): Populates $_POST[$key][$lang] = $val (or $_POST[$key]['nolang'] = $val when uselang == 0).
  • 'scalar' (p_OrderPriority, p_foruser, p_quantity): Populates $_POST[$key] = (string)$val.
  • 'boolean_1_0' (p_published): Populates $_POST[$key] = $this->aiBool($val) ? '1' : '0'.
  • 'boolean_checkbox' (p_group): Sets $_POST[$key] = 1 when true, or unset($_POST[$key]) when false (matching HTML checkbox behavior).

Complete Reference Matrix of All items Plugins & Their AI Behavior

Plugin Mechanism Key Format (getInputName()) AI Type ai_get() Output ai_prepare() & Validation Behavior ai_filter() Mapping
p_inputbox Declarative + Custom File Upload p_inputbox-<id> string | number | boolean | select | file Scalar string/number, array (if multiple), or file URL Validates select options against values; for type=file downloads HTTP(S) URL or local path into $_FILES. gte → fromvalue<id>
lte → tovalue<id>
eq → thisvalue<id>
p_textbox Declarative (ai_field) p_textbox-<id> html | textarea String (or {lang: text} when lang=all) Populates $_POST["p_textbox-<id>"][$lang] (or ['nolang']). Enforces required option. Full-text search via query param
p_photos Custom (modules/p_photos.php) p_photos-<id> photos (multiple: true) [{"id": 210, "url": "/cache/files/....jpg"}, ...] Accepts ordered list of existing photo IDs (kept) and HTTP(S) URLs (downloaded via SSRF-safe _aiFetchImage() into $_FILES). Any existing photo ID omitted from the list is marked $_POST["delete<id>"][<pid>] = 1. Reorders in postSaveNode()! None
p_price Custom (modules/p_price.php) p_pricevalue-<id> or p_pricevalue-<id>-<relid> price | money float (e.g. 2200) When usegroup is off, sets $_POST["p_pricevalue-<id>"]. When usegroup is on (e.g. shipping per carrier #1381), exposes one field per carrier item and sets $_POST["p_pricevalue-<id>"][<relid>]. gte → fromvalue<id>
lte → tovalue<id>
eq → thisvalue<id>
p_group Declarative (ai_field) p_group-<id> boolean true | false Only exposed when editable == 1 (hidden when used purely as the structural group filter of items). Sets/unsets checkbox $_POST["p_group-<id>"]. Automatic via sql_hook
p_group_add Custom (modules/p_group_add.php) p_group_add-<id> relation int[] of related item IDs Exposes target (linked p_group module ID) and inline values: {id: name} when ≤ 50 target items exist. Validates every submitted ID against getGroupItems() and populates $_POST["p_group_add-<id>"]. eq → filter<target>
p_date Declarative + Custom Prepare/Filter p_date-<id> date | datetime "YYYY-MM-DD" (from sitemap.edate) Validates ISO date string via strtotime() and populates $_POST['node']['edate']. gte → from_p_date<id>
lte → to_p_date<id>
p_published Declarative + Custom Filter p_published-<id> boolean true | false (from sitemap.published) Populates $_POST["p_published-<id>"] = '1' | '0'. eq → published<id> ('1' or '2' for unpublished)
p_OrderPriority, p_foruser, p_quantity Declarative (ai_field) <modname>-<id> number / select Number or selected user ID Populates scalar $_POST[$key]. Standard filter map if configured
p_availability, p_user, p_rating, p_comments Declarative (readonly) <modname>-<id> text / number Read-only computed value Marked readonly: true (`when => false` for edit); cannot be overwritten via save_item. None

3. What items Actually Presents to the Agent: Live Outputs from #15305 (News) & #636 (Products)

Click between the tabs below to inspect the actual verbatim JSON responses returned by cms_call.py on managedev.netsystem.gr:

$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 describe
Action 'describe' on module #15305 executed successfully.
Response: {
  "module_id": 15305,
  "title": "Νέα - News",
  "can_edit": true,
  "can_delete": true,
  "fields": [
    {
      "key": "name",
      "label": "Όνομα",
      "type": "text",
      "multilang": true,
      "required": true,
      "readonly": false
    },
    {
      "key": "p_date-15297",
      "label": "Ημερομηνία",
      "type": "date",
      "multilang": false,
      "required": false,
      "readonly": false,
      "filter": { "gte": "from_p_date15297", "lte": "to_p_date15297" }
    },
    {
      "key": "p_textbox-15298",
      "label": "Περιγραφή",
      "type": "html",
      "multilang": true,
      "required": false,
      "readonly": false
    },
    {
      "key": "p_photos-15414",
      "label": "Φωτογραφία",
      "type": "photos",
      "multilang": false,
      "required": false,
      "readonly": false,
      "multiple": true,
      "description": "Ταξινομημένη λίστα με ids υπαρχουσών φωτογραφιών (διατηρούνται) και/ή http(s) URLs εικόνων (προστίθενται). Οι φωτογραφίες που δεν υπάρχουν στη λίστα διαγράφονται, οπότε για να προστεθεί μία διατηρώντας τις υπάρχουσες, συμπεριλάβετε και τα ids τους. Κενή λίστα διαγράφει όλες"
    },
    { "key": "p_photos-15415", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
    { "key": "p_photos-15416", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
    { "key": "p_photos-15417", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
    { "key": "p_textbox-15418", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
    { "key": "p_photos-15419", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
    { "key": "p_textbox-15420", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
    { "key": "p_textbox-15421", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
    { "key": "p_textbox-15300", "label": "Περιγραφή", "type": "html", "multilang": true, ... },
    { "key": "p_photos-15295", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... },
    { "key": "p_photos-15301", "label": "Φωτογραφία", "type": "photos", "multiple": true, ... }
  ]
}
SECTION 10

Real Usage Walkthroughs (Actual Prompts, Tool Calls & Responses)

Below are three end-to-end execution traces captured from actual agent sessions on managedev.netsystem.gr (from /var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/ and live executions). Expand any trace to inspect the exact commands and responses.

TRACE 01 · Brain 15c5b07d Creating & Populating a Multi-Photo News Article in items #15305 + Live Browser Verification

User Prompt: "Φτιάξε ένα νέο άρθρο στα Νέα με το ρεπορτάζ και τις φωτογραφίες του γάμου και έλεγξέ το στον browser."

# Step 1: Agent introspects items module #15305
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 describe
Action 'describe' on module #15305 executed successfully.
Response: {"module_id":15305,"title":"Νέα - News","can_edit":true,"can_delete":true,"fields":[...]}

# Step 2: Agent inspects existing articles to see how fields (p_date-15297, p_textbox-15298, p_photos-15295, p_photos-15301) are structured
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 list_items limit=2
Action 'list_items' on module #15305 executed successfully.
Response: {"total":3,"count":2,"offset":0,"items":[{"id":15333,"url":"\\/?section=7&language=el_GR&itemid15305=15333&detail15305=1"},{"id":15318,"url":"\\/?section=7&language=el_GR&itemid15305=15318&detail15305=1"}]}

# Step 3: Agent creates the new item via save_item, passing external/local image URLs to p_photos-15295 and p_photos-15301
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 15305 save_item \\
  'values={"name":"Ο γάμος της Ελισάβετ Μπάρλου και του Γιώργου Σγούρα που έγινε viral στην Πάτρα","p_date-15297":"2026-10-14","p_textbox-15298":"

Μια λαμπερή βραδιά γεμάτη αγάπη...

","p_textbox-15418":"

Μια βραδιά που έφτασε μέχρι τον ουρανό

...","p_textbox-15420":"

Ένας γάμος με αλήθεια

...","p_textbox-15300":"

Ο γάμος της Ελισάβετ Μπάρλου...

","p_photos-15295":["https://www.thebest.gr/.../1.jpg","https://www.thebest.gr/.../2.jpg"],"p_photos-15301":["https://www.thebest.gr/.../3.jpg"]}' Action 'save_item' on module #15305 executed successfully. Response: { "status": "ok", "id": 99627, "item": { "id": 99627, "url": "/?section=7&language=el_GR&itemid15305=99627&detail15305=1", "p_date-15297": "2026-10-14", "p_photos-15295": [ {"id": 210, "url": "/cache/files/1721d3bc3c20efb520cc8aaeadaf53b6990ae6cd.jpg"}, {"id": 211, "url": "/cache/files/b313e7c3cda3a1627f23694b872ce5f400041558.jpg"} ], "p_photos-15301": [ {"id": 212, "url": "/cache/files/f889061998230cac32539ddfabb0c077ed7727cc.jpg"} ] } } # Step 4: Agent authenticates the isolated Chrome context and verifies the rendered article $ /var/lib/ns-ai-daemon/browser.py managedev.netsystem.gr auth Authenticated browser for managedev.netsystem.gr (3 cookies injected: PHPSESSID, permpin, token). # Step 5: Agent calls chrome-devtools MCP: # -> select_page(pageId=CMS_BROWSER_TARGET_ID) # -> navigate_page(url="https://managedev.netsystem.gr/?section=7&language=el_GR&itemid15305=99627&detail15305=1") # -> take_snapshot() / take_screenshot() to confirm all photos and typography render cleanly.
TRACE 02 · Brain bcd42267 & Live Section #100021 Creating a New Page in treemenu #10 & Publishing Interactive HTML/CSS/JS in htmlbox #341

Goal: Inspect existing theme & navigation tree, create a new top-level page under parent_id=734, and populate its htmlbox #341 with HTML, scoped CSS (.htmlbox341 ...), and interactive JavaScript.

# Step 1: Inspect site theme settings via sitelook #134
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 get_settings
Action 'get_settings' on module #134 executed successfully.
Response: {"section":0,"vars":{"bodybackcolor":{"value":"#f8fafd","css_var":"var(--bodybackcolor)"},"fontfamily":{"value":"'Google Sans', 'Roboto', -apple-system, sans-serif","css_var":"var(--fontfamily)"},...},"layout":"default","class":"771"}

# Step 2: Inspect menu structure via treemenu #10
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 10 get_tree depth=1
Action 'get_tree' on module #10 executed successfully.
Response: {"status":"ok","startpage":"734","parent_id":"734","tree":[...],"total_nodes":20}

# Step 3: Create the 'Chat walkthrough' page under parent_id=734 with layout=onecol and class=524
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 10 add_node \\
  parent_id=734 'name={"el":"Chat walkthrough","en":"Chat walkthrough"}' \\
  visible=1 published=1 layout=onecol class=524
Action 'add_node' on module #10 executed successfully.
Response: {"status":"ok","id":100021,"parent_id":734,"name":{"el":"Chat walkthrough","en":"Chat walkthrough"}}

# Step 4: Populate htmlbox #341 on section=100021 with HTML, scoped CSS, and JS
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 341 set_content \\
  section=100021 lang=el content="...HTML..." css=".htmlbox341 .cw-root { ... }" js="..."
Action 'set_content' on module #341 executed successfully.
Response: {"status":"ok","moduleid":341,"section":100021,"language":"el","container_class":"htmlbox341","updated":["content","css","js"]}
TRACE 03 · Brain 7578dd2d Processing User Attachments & Publishing Generated Files Back to Chat via share_file

Flow: When a user attaches an image or asks the agent to generate a downloadable file/image in chat:

# 1. User uploads an attachment in ns_ai_chat.js -> PHP chat_stream encodes it as base64
# 2. ns_ai_daemon.py materializes it at:
#    /var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/uploads/<stream_id>/<filename>
#    and injects <attached_files> into the prompt.

# 3. To share a generated or processed file back in the chat bubble, the agent base64-encodes
#    the file from its session tmp/<stream_id>/ workspace and calls share_file on the chat module:
$ B64=$(base64 -w 0 /var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/architecture-export.png)
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 21319 share_file \\
  filename="architecture-export.png" content_base64="$B64"
Action 'share_file' on module #21319 executed successfully.
Response: {
  "status": "ok",
  "filename": "architecture-export.png",
  "is_image": true,
  "url": "https://managedev.netsystem.gr/cache/files/ai_shared/7cd66de9c26b910e58bf40d1d440326a/architecture-export.png",
  "markdown": "![architecture-export.png](https://managedev.netsystem.gr/cache/files/ai_shared/7cd66de9c26b910e58bf40d1d440326a/architecture-export.png)"
}

# 4. The agent includes the returned `markdown` in its response text, and ns_ai_chat.js renders it inline!
TRACE 04 · CMS-Native Page Composition Composing a Custom Page with _pagemodules, items%gallery_w_caption, Cloned forms, and site_images/

Goal: Build a custom page using native CMS modules instead of monolithic raw HTML/JS: insert a dedicated htmlbox, create a native PhotoSwipe gallery (items%gallery_w_caption) and upload photos + captions into the CMS, clone an existing contact forms module, and manage theme assets in site_images/.

# Step 1: Inspect page modules & layout positions via kernel module _pagemodules
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules list_page_modules section=25627

# Step 2: Add a fresh standalone htmlbox & populate its content (auto-defaults to parent page section)
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
  section=25627 module=htmlbox position=rightytop
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 100255 set_content \
  content="<div class='intro'>...</div>"

# Step 3: Add a native CMS photo gallery (items%gallery_w_caption) with custom header_label
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
  section=25627 module=items%gallery_w_caption position=rightytop \
  'options={"header_label":{"el":"Φωτογραφίες","en":"Photos"}}'
# -> Returns module_id=100256 and fields: ["p_photos-100266", "p_inputbox-100270", "section"]

# Step 4: Upload photos (via URL or @filepath) & captions into the gallery
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 100256 save_item \
  p_photos-100266=@/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/photo1.png \
  "p_inputbox-100270=Δοκιμαστική Λεζάντα"

# Step 5: Discover existing forms modules & clone forms #119 onto the page
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules find_modules module=forms
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr _pagemodules add_module \
  section=25627 clone_id=119 position=rightytop

# Step 6: Upload or inspect theme assets in site_images/ via sitelook #134
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 list_site_images
$ /var/lib/ns-ai-daemon/cms_call.py managedev.netsystem.gr 134 upload_site_image \
  name=logo.png file=@/var/lib/ns-ai-daemon/projects/managedev.netsystem.gr/tmp/$CMS_STREAM_ID/logo.png
SECTION 11

Prompt Engineering Audit & Improvements (6-Axis Framework & 24 Fixes)

By auditing the prompt-generating code across localib/libmod.php, modules/ns_ai_chat.php, modules/registry/*_reg.php, and /usr/local/bin/ns_ai_daemon.py alongside real execution transcripts in /var/lib/ns-ai-daemon/.gemini/antigravity-cli/brain/, we defined a 6-axis framework for prompt quality and resolved 24 concrete issues.

1. Defining "Prompt Improvement" (6 Measurable Axes)

Axis 1 · Accuracy Contract Fidelity

Zero stale action names (e.g. set_content instead of removed htmlbox_change), clean page URLs via getURL(0) instead of leaking /action/21319/stream_chat, and filtering optional_modules against already-configured modules.

Axis 2 · Boundaries Decision-Boundary Clarity

Explicit rules distinguishing per-page htmlbox instances (scope: page-specific) from global htmlbox instances (scope: global all-pages), editable sitelook variables vs. read-only metavars, and public site DOM vs. the #adminbar CMS toolbar.

Axis 3 · Efficiency Signal-to-Noise & Multi-Turn Delta

Eliminating 2x–5x intra-prompt repetition in items_reg.php, sitelook_reg.php, and htmlbox_reg.php, and replacing full prompt re-injection on follow-up turns with build_follow_up_instructions() (-88.4% size on Turn 2+).

Axis 4 · Ergonomics Zero-Discovery Ergonomics

Providing fallback getHumanID() titles and htmlbox scope/layer traits upfront, exact Chrome DevTools MCP parameter names (filePath, function, targetId), and noting that tmp/<stream_id>/ is pre-created.

Axis 5 · Guardrails Operational & Sandbox Guardrails

Replacing the misleading "use curl to download pages" instruction with a strict rule to use cms_call.py and Chrome DevTools for the target domain, and explicitly forbidding reading /vhosts/ source files.

Axis 6 · Completeness Preset & Module Completeness

Including treemenu, items, and siteconfig in the site_design preset's optional_modules and auto-including _pagemodules for CMS-native page/module composition.

2. Summary of the 24 Prompt & Runtime Fixes

# Component Observed Defect (Before) Implemented Fix (After)
01 ns_ai_daemon.py 4 command examples instructed the agent to run the removed htmlbox_change action. Updated all examples in build_system_instructions() to use set_content.
02 localib/libmod.php ns_ai_build_context() used getSiteURI(), which during stream_chat resolved to /action/21319/stream_chat?section=21316. Replaced getSiteURI() with str_replace('&amp;', '&', getURL(0)), stripping tmpvars / /action/... and resolving the clean page URL.
03 ns_ai_daemon.py optional_modules was not filtered against configured_module_names, falsely telling the agent htmlbox was unconfigured. Filtered optional_modules against configured_module_names so only unconfigured optional modules (e.g. siteconfig) are listed.
04 ns_ai_daemon.py Line 289 told the agent to "use curl to download pages", causing it in transcript 7e5a709f to scrape CMS HTML via curl | grep. Instructed the agent to always use cms_call.py and Chrome DevTools for the CMS domain and never scrape it with curl.
05 libmod.php & ns_ai_chat.php In transcript 6a8a2658, the agent wrote page-specific CSS/JS into global modules #15434/#15435 instead of page-scoped #341. Annotated every htmlbox instance with [scope: page-specific | global all-pages; layers: ...] and added explicit per-page vs. global rules.
06 ns_ai_chat.php & ns_ai_daemon.py In transcript 5dc5aecf, after running browser.py auth, the agent mistook #adminbar for the public site menu and styled it. Added explicit rules in both site_design and browser guidelines that #adminbar is the CMS admin toolbar and must be ignored.
07 ns_ai_daemon.py Wrote ambiguous file=... or filePath=..., omitted session_target_id, and didn't mention HttpOnly cookies (wasting 2–4 tool calls per run). Specified exact MCP parameter names (filePath, function), injected targetId, and documented HttpOnly cookie behavior.
08 ns_ai_daemon.py Re-prepended the full ~12.8 KB system prompt on every turn of multi-turn conversations (e.g. 17 times in transcript 8efc1ed0). Added build_follow_up_instructions() for conversation_id turns, reducing follow-up prompt size from ~12.8 KB to ~1.5 KB (-88.4%).
09 modules/registry/*_reg.php items_reg.php duplicated parameter signatures in description and produced .. Parameters:; sitelook/htmlbox repeated boilerplate. Deduplicated items, sitelook, htmlbox, and siteconfig registry descriptions and stripped trailing periods in the daemon.
10 ns_ai_daemon.py Did not mention that tmp/<stream_id>/ is pre-created by the daemon, causing the agent to run redundant mkdir -p tool calls. Explicitly noted that tmp/<stream_id>/ is already pre-created (do NOT run mkdir -p).
11 modules/ns_ai_chat.php site_design preset omitted treemenu, items, and siteconfig from optional_modules and preset instructions. Added treemenu, items, and siteconfig to site_design with workflow guidance (e.g. using add_node's returned id as section).
12 modules/htmlbox.php In live trace e636546b (Steps 2, 40–44), get_content on global htmlbox #15434 returned section: 7 (empty) because index.php sets $_GET['section'] = 7, bypassing showtoallsections=1. Updated htmlbox::get_content() so showtoallsections=1 resolves to section: 0 (matching set_content) when no explicit section is passed, and always includes the layer key on single-layer boxes.
13 ns_ai_daemon.py (ChromeManager) In trace e636546b (Steps 5–10, 35–39, 155–169), navigate_page(url=...) failed without integer pageId, and list_pages() exposed a shared default-profile tab (pageId=1) with stale cookies/localStorage alongside the isolated tab. create_session_context() now closes any non-isolated default-profile tab while a session is active (so pageId=1 is guaranteed to be the isolated about:blank#<session_id> tab) and documents pageId=1 on all MCP tool signatures.
14 modules/ns_ai_chat.php In trace e636546b (Steps 21, 49–54), the rule to scope all CSS under .htmlbox<id> contradicted global CSS-only boxes (#15434, which has no HTML wrapper), and the agent spent 6 steps checking why CSS was in /cache/files/<hash>.css instead of inline <style>. Distinguished HTML-bearing component boxes (scope with .htmlbox<id>) from global CSS-only boxes (target site-wide or .ns-... selectors directly) and documented /cache/files/<hash>.css bundling.
15 cms_call.py & ns_ai_daemon.py In trace e636546b (Steps 23, 67, 100, 137, 158, 174), because cms_call.py lacked @filepath support, the agent read cms_call.py and regenerated a 542-line Python wrapper script 5 times (~80 KB of output tokens). Added key=@filepath support to cms_call.py (e.g. css=@tmp/<session>/style.css) and instructed the agent to edit raw .html/.css/.js files with replace_file_content.
16 ns_ai_daemon.py In trace e636546b (Steps 93, 130, 151, 188), agy's base system prompt caused the agent to output broken [htmlbox #15434](file:///var/lib/ns-ai-daemon/...) links in web chat responses. Added a strict RESPONSE FORMATTING RULE forbidding file:///... links and internal /var/lib/ns-ai-daemon/... paths in user-facing web chat replies.
17 ns_ai_daemon.py In trace e636546b (Steps 55–64), the agent made 5 sequential evaluate_script round-trips to inspect one level of DOM .children at a time. Added a browser efficiency guideline to combine DOM hierarchy and computed-style inspections into a single evaluate_script call.
18 modules/ns_ai_chat.php & treemenu_reg.php In trace 6dcef567 (Steps 48–64), htmlbox #341 did not render on a page with bare skeleton class 772 (Δύο στήλες), and the vague canhavecontent description misled the agent into thinking canhavecontent=0 blocked page content. Added a full explanation of Page Types (class) and inheritance, a reference list of common 2-column Page Types (524, 1386, 1542, 1519, 1515, 1517 grid layout, and base skeleton classes 1/49/771/772), a note on class-level treemenu instances, and clarified that canhavecontent only applies to parent menu items with child sub-pages (typically 0 = use menu default).
19 ns_ai_daemon.py In trace 6dcef567 (Steps 5–38), scraping an external Elementor site via evaluate_script returning raw innerHTML/outerHTML exceeded tool output limits and spilled to disk 8 times in a row (+16 steps). Instructed the agent to return compact structured data objects (headings, copy, image/background URLs, links, form fields, styles) from evaluate_script and never return raw document.body.innerHTML or oversized outerHTML strings (>4 KB).
20 ns_ai_daemon.py In trace 6dcef567 (Steps 93–110, 145–147), below-the-fold <img loading="lazy"> tags remained unloaded during take_screenshot(fullPage=true) (causing 14 debugging steps), and handle_dialog was called with accept=true instead of action="accept". Documented the headless Chrome loading="lazy" behavior on full-page screenshots and added handle_dialog(pageId=1, action="accept"|"dismiss") to the MCP tool reference.
21 _pagemodules.php, _pagemodules_reg.php & layout_func.php In trace 6dcef567, the agent had no AI actions to inspect, add, clone, configure, reorder, or delete modules on a page, forcing it to cram custom HTML/JS galleries and forms into a single shared htmlbox #341. Exposed 7 AI actions on kernel module _pagemodules (list_page_modules, find_modules, add_module, get_module_options, set_module_options, reorder_module, delete_module), auto-included in the site_design preset and routable via cms_call.py <domain> _pagemodules <action>.
22 items.php, base_func.php, p_photos.php & p_inputbox.php Standalone items%gallery_w_caption modules have useparentitemidascat=1 (which broke aiFields/save_item when no parent items existed) and minshowlevel=999 on caption p_inputbox (hiding it from ai_fields). Added items::usesParentItemAsCat() (checking is_object($this->parent)), exposed edit-only fields when $this->canEdit() is true, added base64 data-URI (@filepath) image upload in p_photos, and supported partial-language syncwithitemname updates in p_inputbox::saveNode().
23 sitelook.php, sitelook_reg.php & cms_call.py The agent had no way to inspect or upload theme/layout images in site_images/ (such as logo.png or background assets) when styling a site. Added list_site_images, upload_site_image (accepting HTTP(S) URLs or local @filepath images via cms_call.py base64 data-URI encoding), and delete_site_image to sitelook.
24 htmlbox.php & ns_ai_chat.php Newly created standalone htmlbox modules on a page defaulted to $homepage (7) when called via cms_call.py without an explicit section parameter. Updated htmlbox::getSection() and htmlbox::get_content() to default to the module's parent page ID when $homepage is true, and added CMS-native page composition, curated module names, and layout position guidance to site_design.
NetSystem CMS AI Subsystem Walkthrough · Generated & verified live on managedev.netsystem.gr (Section #100021, Module htmlbox #341).