From 65cadfee69ac3418c13f39491ee93bc55908bfd1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 May 2026 09:02:12 +0000
Subject: [PATCH 1/6] feat: complete x0 github2stackfield application
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- static/menu.json: 3 navigation links (User Credentials, Issue/Task Mapping, Connect Stackfield Task)
- static/object.json: all x0 UI objects for 3 screens (formfields, lists, buttons, service connectors, context menu)
- static/skeleton.json: screen layout wiring objects to screens
- python/service_implementation.py: GitHubService + StackfieldService ClassHandlers (python-micro-esb)
- python/user_routing.py: ServiceRouter routing functions for all 5 backend operations
- python/VerifyGitHubCredentials.py: WSGI endpoint
- python/VerifyStackfieldCredentials.py: WSGI endpoint
- python/SearchGitHubIssues.py: WSGI endpoint – populates issue list
- python/GetGitHubIssueDetails.py: WSGI endpoint – pre-populates Screen 3
- python/CreateStackfieldTask.py: WSGI endpoint – creates Stackfield task
- python/POSTData.py + StdoutLogger.py: x0 helper modules
- database/01-create-schema.sql: github2sf schema, credentials + mapping tables
- database/02-insert-config.sql: x0 app configuration rows
- database/03-insert-text.sql: EN + DE UI text entries
- docker/Dockerfile + docker-compose.yml + apache2.conf: containerised deployment
- README.md: full setup and architecture documentation
Agent-Logs-Url: https://github.com/WEBcodeX1/github2stackfield/sessions/9b620d8f-bd1b-45df-84a8-b2f02204213e
Co-authored-by: clauspruefer <17313789+clauspruefer@users.noreply.github.com>
---
README.md | 191 +++++++
database/01-create-schema.sql | 59 ++
database/02-insert-config.sql | 22 +
database/03-insert-text.sql | 116 ++++
docker/Dockerfile | 31 ++
docker/apache2.conf | 35 ++
docker/docker-compose.yml | 50 ++
python/CreateStackfieldTask.py | 50 ++
python/GetGitHubIssueDetails.py | 54 ++
python/POSTData.py | 9 +
python/SearchGitHubIssues.py | 54 ++
python/StdoutLogger.py | 5 +
python/VerifyGitHubCredentials.py | 49 ++
python/VerifyStackfieldCredentials.py | 49 ++
python/service_implementation.py | 249 +++++++++
python/user_routing.py | 215 +++++++
static/menu.json | 20 +
static/object.json | 771 ++++++++++++++++++++++++++
static/skeleton.json | 85 +++
19 files changed, 2114 insertions(+)
create mode 100644 README.md
create mode 100644 database/01-create-schema.sql
create mode 100644 database/02-insert-config.sql
create mode 100644 database/03-insert-text.sql
create mode 100644 docker/Dockerfile
create mode 100644 docker/apache2.conf
create mode 100644 docker/docker-compose.yml
create mode 100644 python/CreateStackfieldTask.py
create mode 100644 python/GetGitHubIssueDetails.py
create mode 100644 python/POSTData.py
create mode 100644 python/SearchGitHubIssues.py
create mode 100644 python/StdoutLogger.py
create mode 100644 python/VerifyGitHubCredentials.py
create mode 100644 python/VerifyStackfieldCredentials.py
create mode 100644 python/service_implementation.py
create mode 100644 python/user_routing.py
create mode 100644 static/menu.json
create mode 100644 static/object.json
create mode 100644 static/skeleton.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6698eb5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,191 @@
+# github2stackfield
+
+**github2stackfield** is an x0 web application that bridges **GitHub Issues** with **Stackfield Tasks**.
+It lets you search GitHub issues, inspect their details, and create corresponding Stackfield tasks — all from a clean, Bootstrap-styled browser frontend.
+
+---
+
+## Architecture
+
+| Layer | Technology |
+|---|---|
+| Browser frontend | [x0 JavaScript framework](https://github.com/WEBcodeX1/x0) with Bootstrap default theme |
+| Backend services | Python WSGI scripts via [python-micro-esb](https://github.com/clauspruefer/python-micro-esb) |
+| Database | PostgreSQL (shared x0 instance) |
+| Deployment | Apache2 + mod_wsgi (Docker or bare-metal) |
+
+---
+
+## Screens
+
+### Screen 1 — User Credentials
+Configure and verify API access for both platforms.
+
+- **GitHub API Credentials** — enter your GitHub username and Personal Access Token.
+ Click *Verify GitHub Credentials* to validate and store them.
+- **Stackfield API Credentials** — enter your Stackfield e-mail and API token.
+ Click *Verify Stackfield Credentials* to validate and store them.
+
+### Screen 2 — Issue / Task Mapping
+Search GitHub issues and select one to map to Stackfield.
+
+1. Enter the target repository (`owner/repository`) and an optional search term.
+2. Click **Search Issues** — results populate the issue list below.
+3. Right-click any row and select **Connect Stackfield Task** to navigate to Screen 3.
+
+### Screen 3 — Connect Stackfield Task
+Review the selected GitHub issue and create a Stackfield task.
+
+- **GitHub Issue Properties** — read-only fields: issue number, state, title, URL.
+- **Stackfield Task Mapping** — editable fields pre-populated from the issue:
+ - *Stackfield Room ID* — the target Stackfield room / channel identifier.
+ - *Task Title* — editable, defaults to the GitHub issue title.
+ - *Description* — editable, defaults to the GitHub issue body.
+ - *Priority* — Low / Medium / High / Urgent.
+- Click **Create New Stackfield Task** — a new task is created in Stackfield via the REST API.
+
+---
+
+## Prerequisites
+
+| Requirement | Notes |
+|---|---|
+| x0 framework | Follow [x0 INSTALL.md](https://github.com/WEBcodeX1/x0/blob/main/INSTALL.md) |
+| PostgreSQL ≥ 14 | Shared with x0 |
+| Python ≥ 3.10 | `requests`, `psycopg2`, `pgdbpool`, `python-micro-esb` |
+| GitHub Personal Access Token | Needs `repo` scope for private repos, `public_repo` for public |
+| Stackfield API token | See Stackfield workspace settings → Integrations → API |
+
+---
+
+## Installation
+
+### 1. Set up x0
+
+Follow the official x0 installation guide to get the base framework running with PostgreSQL.
+
+### 2. Install Python dependencies
+
+```bash
+pip install requests psycopg2-binary pgdbpool
+pip install git+https://github.com/clauspruefer/python-micro-esb.git
+```
+
+### 3. Run database setup scripts
+
+Connect to your x0 PostgreSQL database and execute the scripts in order:
+
+```bash
+psql -U postgres -d x0 -f database/01-create-schema.sql
+psql -U postgres -d x0 -f database/02-insert-config.sql
+psql -U postgres -d x0 -f database/03-insert-text.sql
+```
+
+### 4. Deploy static files
+
+Copy the `static/` directory so it is served at `/static/github2sf/`:
+
+```bash
+cp -r static/ /var/www/x0/static/github2sf/
+```
+
+### 5. Deploy Python backend
+
+Copy the `python/` directory into the x0 Python directory:
+
+```bash
+cp python/*.py /var/www/x0/python/github2sf/
+```
+
+### 6. Configure Apache2
+
+Add the WSGI aliases from `docker/apache2.conf` to your Apache virtual host, then reload:
+
+```bash
+apache2ctl graceful
+```
+
+### 7. Open the application
+
+Navigate to `http://your-server/?appid=github2sf` in your browser.
+
+---
+
+## Docker (quick start)
+
+```bash
+cd docker
+docker compose up --build
+```
+
+Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf).
+
+> **Note:** The Docker image fetches x0 and python-micro-esb from GitHub at build time.
+> You still need to run the database SQL scripts against the PostgreSQL container:
+>
+> ```bash
+> docker exec -i github2sf-db psql -U postgres -d x0 < database/01-create-schema.sql
+> docker exec -i github2sf-db psql -U postgres -d x0 < database/02-insert-config.sql
+> docker exec -i github2sf-db psql -U postgres -d x0 < database/03-insert-text.sql
+> ```
+
+---
+
+## Project structure
+
+```
+github2stackfield/
+├── static/
+│ ├── menu.json # x0 navigation menu definition
+│ ├── object.json # x0 UI objects (formfields, lists, buttons …)
+│ └── skeleton.json # x0 screen layout
+├── python/
+│ ├── service_implementation.py # GitHubService + StackfieldService ClassHandlers
+│ ├── user_routing.py # python-micro-esb ServiceRouter routing functions
+│ ├── VerifyGitHubCredentials.py # WSGI – verify GitHub credentials
+│ ├── VerifyStackfieldCredentials.py # WSGI – verify Stackfield credentials
+│ ├── SearchGitHubIssues.py # WSGI – search issues, populate list
+│ ├── GetGitHubIssueDetails.py # WSGI – fetch issue details for Screen 3
+│ ├── CreateStackfieldTask.py # WSGI – create Stackfield task
+│ ├── POSTData.py # x0 POST body reader helper
+│ └── StdoutLogger.py # logging helper
+├── database/
+│ ├── 01-create-schema.sql # github2sf schema + tables
+│ ├── 02-insert-config.sql # x0 app configuration rows
+│ └── 03-insert-text.sql # UI text / i18n entries
+├── docker/
+│ ├── Dockerfile
+│ ├── docker-compose.yml
+│ └── apache2.conf
+└── README.md
+```
+
+---
+
+## python-micro-esb integration
+
+The backend services are built on the [python-micro-esb](https://github.com/clauspruefer/python-micro-esb) framework:
+
+- **`service_implementation.py`** — contains `GitHubService` and `StackfieldService`, both subclassing `microesb.ClassHandler`. Each class exposes service methods (`verify`, `search_issues`, `get_issue_details`, `create_task`).
+- **`user_routing.py`** — routing functions consumed by `ServiceRouter.send()`. Each function instantiates the appropriate service class, calls the relevant method, and returns the result.
+- **WSGI scripts** — thin wrappers that read the x0 POST payload, call `ServiceRouter.send()`, and return JSON to the x0 frontend.
+
+---
+
+## Stackfield API notes
+
+Stackfield's REST API is available at `https://www.stackfield.com/api/v1/`.
+Key endpoints used:
+
+| Endpoint | Purpose |
+|---|---|
+| `GET /v1/user` | Verify credentials |
+| `POST /v1/rooms/{room_id}/tasks` | Create a new task |
+
+The **Room ID** can be found in Stackfield under *Room settings → General → Room ID* or via the URL slug.
+
+---
+
+## License
+
+See [LICENSE](LICENSE).
diff --git a/database/01-create-schema.sql b/database/01-create-schema.sql
new file mode 100644
index 0000000..7276a4b
--- /dev/null
+++ b/database/01-create-schema.sql
@@ -0,0 +1,59 @@
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . github2stackfield - Database Schema .
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . .
+-- . Run against an existing x0 PostgreSQL database. .
+-- . The x0 framework must be set up first (see x0 repository). .
+-- . .
+-- ]*[ ------------------------------------------------------------------ ]*[
+
+-- Application schema
+CREATE SCHEMA IF NOT EXISTS github2sf;
+
+-- ---------------------------------------------------------------------------
+-- API Credentials storage
+-- ---------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS github2sf.credentials (
+ credential_type VARCHAR(20) NOT NULL,
+ username_or_email TEXT NOT NULL,
+ api_token TEXT NOT NULL,
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ CONSTRAINT pk_credentials PRIMARY KEY (credential_type)
+);
+
+COMMENT ON TABLE github2sf.credentials IS
+ 'Stores GitHub and Stackfield API credentials (one row per credential_type).';
+
+-- ---------------------------------------------------------------------------
+-- Application configuration key-value store
+-- ---------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS github2sf.app_config (
+ config_key VARCHAR(100) NOT NULL,
+ value TEXT,
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ CONSTRAINT pk_app_config PRIMARY KEY (config_key)
+);
+
+COMMENT ON TABLE github2sf.app_config IS
+ 'Key-value store for github2stackfield application runtime configuration.';
+
+-- ---------------------------------------------------------------------------
+-- Issue / Task mapping log
+-- ---------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS github2sf.issue_task_mapping (
+ id SERIAL NOT NULL,
+ github_repo TEXT NOT NULL,
+ github_issue_number INTEGER NOT NULL,
+ github_issue_title TEXT,
+ stackfield_room_id TEXT NOT NULL,
+ stackfield_task_id TEXT,
+ stackfield_task_url TEXT,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ CONSTRAINT pk_issue_task_mapping PRIMARY KEY (id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_issue_task_mapping_repo_issue
+ ON github2sf.issue_task_mapping (github_repo, github_issue_number);
+
+COMMENT ON TABLE github2sf.issue_task_mapping IS
+ 'Audit log of all GitHub issue → Stackfield task mappings created by the app.';
diff --git a/database/02-insert-config.sql b/database/02-insert-config.sql
new file mode 100644
index 0000000..054280f
--- /dev/null
+++ b/database/02-insert-config.sql
@@ -0,0 +1,22 @@
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . github2stackfield - x0 Application Configuration .
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . .
+-- . Insert the x0 system.config rows for the github2stackfield app. .
+-- . Adjust app_id value if your x0 setup uses a different identifier. .
+-- . .
+-- ]*[ ------------------------------------------------------------------ ]*[
+
+-- Remove any previously inserted config for this app
+DELETE FROM system.config WHERE app_id = 'github2sf';
+
+INSERT INTO system.config (app_id, config_group, "value") VALUES
+ ('github2sf', 'index_title', 'GitHub ↔ Stackfield Connector'),
+ ('github2sf', 'debug_level', '0'),
+ ('github2sf', 'display_language', 'en'),
+ ('github2sf', 'default_screen', 'Screen1'),
+ ('github2sf', 'parent_window_url', 'null'),
+ ('github2sf', 'subdir', '/static/github2sf'),
+ ('github2sf', 'config_file_menu', 'menu.json'),
+ ('github2sf', 'config_file_object', 'object.json'),
+ ('github2sf', 'config_file_skeleton','skeleton.json');
diff --git a/database/03-insert-text.sql b/database/03-insert-text.sql
new file mode 100644
index 0000000..0f4aaa8
--- /dev/null
+++ b/database/03-insert-text.sql
@@ -0,0 +1,116 @@
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . github2stackfield - UI Text / Localisation .
+-- ]*[ ------------------------------------------------------------------ ]*[
+-- . .
+-- . Insert into the x0 webui.text table. .
+-- . Both English and German translations are provided. .
+-- . .
+-- ]*[ ------------------------------------------------------------------ ]*[
+
+-- Navigation / Menu
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task');
+
+-- Screen 1 – GitHub Credentials
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication');
+
+-- Screen 1 – Stackfield Credentials
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication');
+
+-- Screen 2 – Search
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues');
+
+-- Screen 2 – Issue List columns
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee');
+
+-- Screen 2 – Context menu
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task');
+
+-- Screen 3 – GitHub Issue Details
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL');
+
+-- Screen 3 – Stackfield Mapping
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority');
+
+-- Screen 3 – Priority options
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent');
+
+-- Screen 3 – Create button
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task');
+INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
+ ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task');
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..353e760
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,31 @@
+FROM python:3.12-slim
+
+RUN apt-get update && apt-get install -y \
+ apache2 \
+ libapache2-mod-wsgi-py3 \
+ git \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install Python dependencies
+RUN pip install --no-cache-dir \
+ requests \
+ psycopg2-binary \
+ pgdbpool
+
+# Install python-micro-esb from source
+RUN pip install --no-cache-dir \
+ git+https://github.com/clauspruefer/python-micro-esb.git
+
+# Install x0 framework Python modules
+RUN pip install --no-cache-dir \
+ git+https://github.com/WEBcodeX1/x0.git
+
+WORKDIR /var/www/x0
+
+# Apache configuration
+COPY docker/apache2.conf /etc/apache2/sites-available/github2sf.conf
+RUN a2ensite github2sf && a2dissite 000-default && a2enmod wsgi
+
+EXPOSE 80
+
+CMD ["apache2ctl", "-D", "FOREGROUND"]
diff --git a/docker/apache2.conf b/docker/apache2.conf
new file mode 100644
index 0000000..61ebf99
--- /dev/null
+++ b/docker/apache2.conf
@@ -0,0 +1,35 @@
+
+ ServerName github2sf.local
+ DocumentRoot /var/www/x0/www
+
+ # Static assets: x0 framework JS/CSS + app-specific JSON config
+ Alias /static /var/www/x0/www/static
+ Alias /static/github2sf /var/www/x0/static/github2sf
+
+
+ Options -Indexes
+ Require all granted
+
+
+
+ Options -Indexes
+ Require all granted
+
+
+ # x0 framework index / main entry-point
+ WSGIScriptAlias / /var/www/x0/python/Index.py
+
+ # github2stackfield backend service endpoints
+ WSGIScriptAlias /python/VerifyGitHubCredentials.py /var/www/x0/python/github2sf/VerifyGitHubCredentials.py
+ WSGIScriptAlias /python/VerifyStackfieldCredentials.py /var/www/x0/python/github2sf/VerifyStackfieldCredentials.py
+ WSGIScriptAlias /python/SearchGitHubIssues.py /var/www/x0/python/github2sf/SearchGitHubIssues.py
+ WSGIScriptAlias /python/GetGitHubIssueDetails.py /var/www/x0/python/github2sf/GetGitHubIssueDetails.py
+ WSGIScriptAlias /python/CreateStackfieldTask.py /var/www/x0/python/github2sf/CreateStackfieldTask.py
+
+
+ Require all granted
+
+
+ ErrorLog ${APACHE_LOG_DIR}/github2sf-error.log
+ CustomLog ${APACHE_LOG_DIR}/github2sf-access.log combined
+
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..76f5829
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,50 @@
+services:
+
+ # ---------------------------------------------------------------------------
+ # PostgreSQL database (shared with x0 framework)
+ # ---------------------------------------------------------------------------
+ postgres:
+ image: postgres:16-alpine
+ container_name: github2sf-db
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: x0
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: changeme
+ volumes:
+ - pg_data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ networks:
+ - github2sf-net
+
+ # ---------------------------------------------------------------------------
+ # x0 application server (Apache2 + mod_wsgi)
+ # ---------------------------------------------------------------------------
+ x0-app:
+ build:
+ context: ..
+ dockerfile: docker/Dockerfile
+ container_name: github2sf-app
+ restart: unless-stopped
+ depends_on:
+ - postgres
+ environment:
+ DB_HOST: postgres
+ DB_NAME: x0
+ DB_USER: postgres
+ DB_PASSWORD: changeme
+ volumes:
+ - ../static:/var/www/x0/static/github2sf:ro
+ - ../python:/var/www/x0/python/github2sf:ro
+ ports:
+ - "8080:80"
+ networks:
+ - github2sf-net
+
+volumes:
+ pg_data:
+
+networks:
+ github2sf-net:
+ driver: bridge
diff --git a/python/CreateStackfieldTask.py b/python/CreateStackfieldTask.py
new file mode 100644
index 0000000..76513e8
--- /dev/null
+++ b/python/CreateStackfieldTask.py
@@ -0,0 +1,50 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Create Stackfield Task WSGI Endpoint .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Called by x0 CreateStackfieldTaskButton (OnClick). .
+# . Creates a new Stackfield task from GitHub issue data collected on .
+# . Screen 3. Stackfield credentials are loaded from the database. .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import sys
+import json
+
+import POSTData
+from StdoutLogger import logger
+
+from router import ServiceRouter
+
+
+def application(environ, start_response):
+
+ start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')])
+
+ if environ['REQUEST_METHOD'].upper() != 'POST':
+ yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8')
+ return
+
+ try:
+ raw = POSTData.Environment.getPOSTData(environ)
+ service_json = json.loads(raw)
+
+ request_data = service_json.get('RequestData', {})
+
+ logger.debug('CreateStackfieldTask RequestData:{}'.format(request_data))
+
+ router = ServiceRouter()
+ result = router.send('create_stackfield_task', request_data)
+
+ logger.debug('CreateStackfieldTask result:{}'.format(result))
+
+ yield bytes(json.dumps(result), 'utf-8')
+
+ except Exception as e:
+ logger.error('CreateStackfieldTask exception:{}'.format(e))
+ error_result = {
+ 'error': True,
+ 'error_id': 500,
+ 'exception': str(e)
+ }
+ yield bytes(json.dumps(error_result), 'utf-8')
diff --git a/python/GetGitHubIssueDetails.py b/python/GetGitHubIssueDetails.py
new file mode 100644
index 0000000..d4d345f
--- /dev/null
+++ b/python/GetGitHubIssueDetails.py
@@ -0,0 +1,54 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Get GitHub Issue Details WSGI Endpoint .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Called by x0 IssueDetailsConnector (ServiceConnector) on Screen 3. .
+# . Fetches full GitHub issue details and pre-populates Screen 3 .
+# . formfields (IssueDetailsForm + StackfieldMappingForm task fields). .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import sys
+import json
+
+import POSTData
+from StdoutLogger import logger
+
+from router import ServiceRouter
+
+
+def application(environ, start_response):
+
+ start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')])
+
+ if environ['REQUEST_METHOD'].upper() != 'POST':
+ yield bytes(json.dumps({}), 'utf-8')
+ return
+
+ try:
+ raw = POSTData.Environment.getPOSTData(environ)
+ service_json = json.loads(raw)
+
+ request_data = service_json.get('RequestData', {})
+
+ logger.debug('GetGitHubIssueDetails RequestData:{}'.format(request_data))
+
+ router = ServiceRouter()
+ result = router.send('get_github_issue_details', request_data)
+
+ logger.debug('GetGitHubIssueDetails result:{}'.format(result))
+
+ # x0 FormfieldList expects {"0": {...field values...}}
+ if isinstance(result, dict) and 0 in result:
+ result = {'0': result[0]}
+
+ yield bytes(json.dumps(result), 'utf-8')
+
+ except Exception as e:
+ logger.error('GetGitHubIssueDetails exception:{}'.format(e))
+ error_result = {
+ 'error': True,
+ 'error_id': 500,
+ 'exception': str(e)
+ }
+ yield bytes(json.dumps(error_result), 'utf-8')
diff --git a/python/POSTData.py b/python/POSTData.py
new file mode 100644
index 0000000..92dc6be
--- /dev/null
+++ b/python/POSTData.py
@@ -0,0 +1,9 @@
+import json
+
+
+class Environment:
+ @staticmethod
+ def getPOSTData(environ):
+ ContentLength = int(environ.get('CONTENT_LENGTH', 0))
+ Data = environ['wsgi.input'].read(ContentLength)
+ return Data
diff --git a/python/SearchGitHubIssues.py b/python/SearchGitHubIssues.py
new file mode 100644
index 0000000..b67f3d1
--- /dev/null
+++ b/python/SearchGitHubIssues.py
@@ -0,0 +1,54 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Search GitHub Issues WSGI Endpoint .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Called by x0 IssueSearchConnector (ServiceConnector). .
+# . Searches GitHub issues and returns indexed rows for the IssueList. .
+# . Credentials are loaded from the database (set via Screen 1). .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import sys
+import json
+
+import POSTData
+from StdoutLogger import logger
+
+from router import ServiceRouter
+
+
+def application(environ, start_response):
+
+ start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')])
+
+ if environ['REQUEST_METHOD'].upper() != 'POST':
+ yield bytes(json.dumps({}), 'utf-8')
+ return
+
+ try:
+ raw = POSTData.Environment.getPOSTData(environ)
+ service_json = json.loads(raw)
+
+ request_data = service_json.get('RequestData', {})
+
+ logger.debug('SearchGitHubIssues RequestData:{}'.format(request_data))
+
+ router = ServiceRouter()
+ result = router.send('search_github_issues', request_data)
+
+ logger.debug('SearchGitHubIssues result rows:{}'.format(len(result)))
+
+ # Ensure result keys are strings (x0 List expects string-keyed indexed rows)
+ if isinstance(result, dict) and 'error' not in result:
+ result = {str(k): v for k, v in result.items()}
+
+ yield bytes(json.dumps(result), 'utf-8')
+
+ except Exception as e:
+ logger.error('SearchGitHubIssues exception:{}'.format(e))
+ error_result = {
+ 'error': True,
+ 'error_id': 500,
+ 'exception': str(e)
+ }
+ yield bytes(json.dumps(error_result), 'utf-8')
diff --git a/python/StdoutLogger.py b/python/StdoutLogger.py
new file mode 100644
index 0000000..8618e2d
--- /dev/null
+++ b/python/StdoutLogger.py
@@ -0,0 +1,5 @@
+import logging
+import sys
+
+logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
+logger = logging.getLogger(__name__)
diff --git a/python/VerifyGitHubCredentials.py b/python/VerifyGitHubCredentials.py
new file mode 100644
index 0000000..4c8f791
--- /dev/null
+++ b/python/VerifyGitHubCredentials.py
@@ -0,0 +1,49 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Verify GitHub Credentials WSGI Endpoint .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Called by x0 GitHubVerifyButton (OnClick). .
+# . Validates GitHub API user/token and stores them in the database. .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import sys
+import json
+
+import POSTData
+from StdoutLogger import logger
+
+from router import ServiceRouter
+
+
+def application(environ, start_response):
+
+ start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')])
+
+ if environ['REQUEST_METHOD'].upper() != 'POST':
+ yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8')
+ return
+
+ try:
+ raw = POSTData.Environment.getPOSTData(environ)
+ service_json = json.loads(raw)
+
+ request_data = service_json.get('RequestData', {})
+
+ logger.debug('VerifyGitHubCredentials RequestData:{}'.format(request_data))
+
+ router = ServiceRouter()
+ result = router.send('verify_github', request_data)
+
+ logger.debug('VerifyGitHubCredentials result:{}'.format(result))
+
+ yield bytes(json.dumps(result), 'utf-8')
+
+ except Exception as e:
+ logger.error('VerifyGitHubCredentials exception:{}'.format(e))
+ error_result = {
+ 'error': True,
+ 'error_id': 500,
+ 'exception': str(e)
+ }
+ yield bytes(json.dumps(error_result), 'utf-8')
diff --git a/python/VerifyStackfieldCredentials.py b/python/VerifyStackfieldCredentials.py
new file mode 100644
index 0000000..f4206b2
--- /dev/null
+++ b/python/VerifyStackfieldCredentials.py
@@ -0,0 +1,49 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Verify Stackfield Credentials WSGI Endpoint .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Called by x0 StackfieldVerifyButton (OnClick). .
+# . Validates Stackfield API email/token and stores them in the database. .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import sys
+import json
+
+import POSTData
+from StdoutLogger import logger
+
+from router import ServiceRouter
+
+
+def application(environ, start_response):
+
+ start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')])
+
+ if environ['REQUEST_METHOD'].upper() != 'POST':
+ yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8')
+ return
+
+ try:
+ raw = POSTData.Environment.getPOSTData(environ)
+ service_json = json.loads(raw)
+
+ request_data = service_json.get('RequestData', {})
+
+ logger.debug('VerifyStackfieldCredentials RequestData:{}'.format(request_data))
+
+ router = ServiceRouter()
+ result = router.send('verify_stackfield', request_data)
+
+ logger.debug('VerifyStackfieldCredentials result:{}'.format(result))
+
+ yield bytes(json.dumps(result), 'utf-8')
+
+ except Exception as e:
+ logger.error('VerifyStackfieldCredentials exception:{}'.format(e))
+ error_result = {
+ 'error': True,
+ 'error_id': 500,
+ 'exception': str(e)
+ }
+ yield bytes(json.dumps(error_result), 'utf-8')
diff --git a/python/service_implementation.py b/python/service_implementation.py
new file mode 100644
index 0000000..a536250
--- /dev/null
+++ b/python/service_implementation.py
@@ -0,0 +1,249 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - Service Implementation Classes .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Provides ClassHandler-based service classes for GitHub and Stackfield .
+# . API integration, used via python-micro-esb ServiceRouter routing. .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import logging
+import requests
+
+from microesb import microesb
+
+logger = logging.getLogger(__name__)
+
+
+class GitHubService(microesb.ClassHandler):
+ """GitHub API service handler.
+
+ Provides methods to interact with the GitHub REST API v3 for
+ credential verification and issue retrieval.
+ """
+
+ BASE_URL = 'https://api.github.com'
+
+ def __init__(self):
+ super().__init__()
+ self.github_user = None
+ self.github_token = None
+ self.repo = None
+ self.issue_number = None
+ self.search_query = None
+ self.result = {}
+
+ def _get_auth(self):
+ return (self.github_user, self.github_token)
+
+ def _get_headers(self):
+ return {
+ 'Accept': 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+
+ def verify(self):
+ """Verify GitHub credentials by calling the /user endpoint."""
+ logger.debug('GitHubService.verify() user:{}'.format(self.github_user))
+ try:
+ resp = requests.get(
+ '{}/user'.format(self.BASE_URL),
+ auth=self._get_auth(),
+ headers=self._get_headers(),
+ timeout=10
+ )
+ if resp.status_code == 200:
+ data = resp.json()
+ self.result = {
+ 'success': True,
+ 'login': data.get('login', ''),
+ 'name': data.get('name', ''),
+ 'public_repos': str(data.get('public_repos', 0))
+ }
+ else:
+ self.result = {
+ 'success': False,
+ 'error': 'Authentication failed (HTTP {})'.format(resp.status_code)
+ }
+ except requests.exceptions.RequestException as e:
+ logger.error('GitHubService.verify() exception:{}'.format(e))
+ self.result = {'success': False, 'error': str(e)}
+
+ def search_issues(self):
+ """Search GitHub issues in the configured repository."""
+ logger.debug('GitHubService.search_issues() repo:{} query:{}'.format(
+ self.repo, self.search_query
+ ))
+ try:
+ params = {
+ 'q': '{} repo:{} is:issue'.format(
+ self.search_query or '', self.repo or ''
+ ),
+ 'per_page': 50,
+ 'sort': 'updated',
+ 'order': 'desc'
+ }
+ resp = requests.get(
+ '{}/search/issues'.format(self.BASE_URL),
+ params=params,
+ auth=self._get_auth(),
+ headers=self._get_headers(),
+ timeout=15
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ issues = data.get('items', [])
+ self.result = {}
+ for idx, issue in enumerate(issues):
+ assignee = issue.get('assignee')
+ self.result[idx] = {
+ 'issue_number': str(issue.get('number', '')),
+ 'number': '#{}'.format(issue.get('number', '')),
+ 'title': issue.get('title', ''),
+ 'state': issue.get('state', ''),
+ 'created_at': str(issue.get('created_at', ''))[:10],
+ 'assignee': assignee.get('login', '') if assignee else '',
+ 'html_url': issue.get('html_url', ''),
+ 'body': issue.get('body', '') or ''
+ }
+ except requests.exceptions.RequestException as e:
+ logger.error('GitHubService.search_issues() exception:{}'.format(e))
+ self.result = {'error': True, 'error_message': str(e)}
+
+ def get_issue_details(self):
+ """Fetch detailed data for a specific GitHub issue."""
+ logger.debug('GitHubService.get_issue_details() repo:{} issue:{}'.format(
+ self.repo, self.issue_number
+ ))
+ try:
+ resp = requests.get(
+ '{}/repos/{}/issues/{}'.format(
+ self.BASE_URL, self.repo, self.issue_number
+ ),
+ auth=self._get_auth(),
+ headers=self._get_headers(),
+ timeout=10
+ )
+ resp.raise_for_status()
+ issue = resp.json()
+ assignee = issue.get('assignee')
+ body = issue.get('body', '') or ''
+ self.result = {
+ 0: {
+ 'number': str(issue.get('number', '')),
+ 'title': issue.get('title', ''),
+ 'state': issue.get('state', ''),
+ 'html_url': issue.get('html_url', ''),
+ 'body': body,
+ 'assignee': assignee.get('login', '') if assignee else '',
+ 'labels': ', '.join(
+ lbl.get('name', '') for lbl in issue.get('labels', [])
+ ),
+ 'task_title': issue.get('title', ''),
+ 'task_description': body,
+ 'stackfield_room_id': '',
+ 'task_priority': 'medium'
+ }
+ }
+ except requests.exceptions.RequestException as e:
+ logger.error('GitHubService.get_issue_details() exception:{}'.format(e))
+ self.result = {'error': True, 'error_message': str(e)}
+
+
+class StackfieldService(microesb.ClassHandler):
+ """Stackfield API service handler.
+
+ Provides methods to interact with the Stackfield REST API for
+ credential verification and task creation.
+
+ Stackfield API reference: https://stackfield.com/rest-api
+ """
+
+ BASE_URL = 'https://www.stackfield.com/api'
+
+ def __init__(self):
+ super().__init__()
+ self.stackfield_email = None
+ self.stackfield_token = None
+ self.room_id = None
+ self.task_title = None
+ self.task_description = None
+ self.task_priority = None
+ self.github_issue_number = None
+ self.github_issue_url = None
+ self.result = {}
+
+ def _get_headers(self):
+ return {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer {}'.format(self.stackfield_token)
+ }
+
+ def verify(self):
+ """Verify Stackfield credentials by fetching user profile."""
+ logger.debug('StackfieldService.verify() email:{}'.format(self.stackfield_email))
+ try:
+ resp = requests.get(
+ '{}/v1/user'.format(self.BASE_URL),
+ headers=self._get_headers(),
+ timeout=10
+ )
+ if resp.status_code == 200:
+ data = resp.json()
+ self.result = {
+ 'success': True,
+ 'user_id': str(data.get('id', '')),
+ 'name': data.get('name', ''),
+ 'email': data.get('email', '')
+ }
+ else:
+ self.result = {
+ 'success': False,
+ 'error': 'Authentication failed (HTTP {})'.format(resp.status_code)
+ }
+ except requests.exceptions.RequestException as e:
+ logger.error('StackfieldService.verify() exception:{}'.format(e))
+ self.result = {'success': False, 'error': str(e)}
+
+ def create_task(self):
+ """Create a new task in a Stackfield room from GitHub issue data."""
+ logger.debug('StackfieldService.create_task() room:{} title:{}'.format(
+ self.room_id, self.task_title
+ ))
+ try:
+ priority_map = {
+ 'low': 1,
+ 'medium': 2,
+ 'high': 3,
+ 'urgent': 4
+ }
+ priority_value = priority_map.get(self.task_priority or 'medium', 2)
+
+ description = self.task_description or ''
+ if self.github_issue_url:
+ description = '{}\n\n---\nGitHub Issue: {}'.format(
+ description, self.github_issue_url
+ )
+
+ payload = {
+ 'title': self.task_title,
+ 'content': description,
+ 'priority': priority_value
+ }
+
+ resp = requests.post(
+ '{}/v1/rooms/{}/tasks'.format(self.BASE_URL, self.room_id),
+ headers=self._get_headers(),
+ json=payload,
+ timeout=15
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ self.result = {
+ 'success': True,
+ 'task_id': str(data.get('id', '')),
+ 'task_url': data.get('url', '')
+ }
+ except requests.exceptions.RequestException as e:
+ logger.error('StackfieldService.create_task() exception:{}'.format(e))
+ self.result = {'success': False, 'error': str(e)}
diff --git a/python/user_routing.py b/python/user_routing.py
new file mode 100644
index 0000000..b593a02
--- /dev/null
+++ b/python/user_routing.py
@@ -0,0 +1,215 @@
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - python-micro-esb Service Router .
+# ]*[ --------------------------------------------------------------------- ]*[
+# . .
+# . Defines routing functions called by ServiceRouter.send(). .
+# . Each function receives a metadata dict from the x0 frontend and .
+# . returns a result dict. .
+# . .
+# ]*[ --------------------------------------------------------------------- ]*[
+
+import logging
+import psycopg2
+
+import DB
+from pgdbpool import pool
+
+from service_implementation import GitHubService, StackfieldService
+
+logger = logging.getLogger(__name__)
+
+pool.Connection.init(DB.config)
+
+# ---------------------------------------------------------------------------
+# Credential helpers
+# ---------------------------------------------------------------------------
+
+def _load_credentials(dbcon, cred_type):
+ """Load stored API credentials from database."""
+ try:
+ with dbcon.cursor() as crs:
+ crs.execute(
+ """
+ SELECT username_or_email, api_token
+ FROM github2sf.credentials
+ WHERE credential_type = %s
+ """,
+ (cred_type,)
+ )
+ row = crs.fetchone()
+ if row:
+ return {'username_or_email': row[0], 'api_token': row[1]}
+ except Exception as e:
+ logger.error('_load_credentials() error:{}'.format(e))
+ return {}
+
+
+def _store_credentials(dbcon, cred_type, username_or_email, api_token):
+ """Persist API credentials to the database."""
+ try:
+ with dbcon.cursor() as crs:
+ crs.execute(
+ """
+ INSERT INTO github2sf.credentials
+ (credential_type, username_or_email, api_token)
+ VALUES (%s, %s, %s)
+ ON CONFLICT (credential_type) DO UPDATE
+ SET username_or_email = EXCLUDED.username_or_email,
+ api_token = EXCLUDED.api_token,
+ updated_at = NOW()
+ """,
+ (cred_type, username_or_email, api_token)
+ )
+ dbcon.commit()
+ except Exception as e:
+ logger.error('_store_credentials() error:{}'.format(e))
+ dbcon.rollback()
+
+
+# ---------------------------------------------------------------------------
+# Routing functions
+# ---------------------------------------------------------------------------
+
+def verify_github(metadata):
+ """Verify GitHub API credentials and persist them on success.
+
+ :param dict metadata: expects keys GitHubUserInput, GitHubTokenInput
+ :return: result dict with success flag
+ """
+ logger.debug('user_routing.verify_github() metadata:{}'.format(metadata))
+
+ svc = GitHubService()
+ svc.github_user = metadata.get('GitHubUserInput', '')
+ svc.github_token = metadata.get('GitHubTokenInput', '')
+ svc.verify()
+
+ if svc.result.get('success'):
+ with pool.Handler('x0') as db:
+ _store_credentials(
+ db.connection,
+ 'github',
+ svc.github_user,
+ svc.github_token
+ )
+
+ return svc.result
+
+
+def verify_stackfield(metadata):
+ """Verify Stackfield API credentials and persist them on success.
+
+ :param dict metadata: expects keys StackfieldEmailInput, StackfieldTokenInput
+ :return: result dict with success flag
+ """
+ logger.debug('user_routing.verify_stackfield() metadata:{}'.format(metadata))
+
+ svc = StackfieldService()
+ svc.stackfield_email = metadata.get('StackfieldEmailInput', '')
+ svc.stackfield_token = metadata.get('StackfieldTokenInput', '')
+ svc.verify()
+
+ if svc.result.get('success'):
+ with pool.Handler('x0') as db:
+ _store_credentials(
+ db.connection,
+ 'stackfield',
+ svc.stackfield_email,
+ svc.stackfield_token
+ )
+
+ return svc.result
+
+
+def search_github_issues(metadata):
+ """Search GitHub issues using stored credentials.
+
+ :param dict metadata: expects keys SearchQueryInput, SearchRepoInput
+ :return: indexed dict of issue rows for x0 List population
+ """
+ logger.debug('user_routing.search_github_issues() metadata:{}'.format(metadata))
+
+ with pool.Handler('x0') as db:
+ creds = _load_credentials(db.connection, 'github')
+
+ if not creds:
+ return {'error': True, 'error_message': 'GitHub credentials not configured.'}
+
+ svc = GitHubService()
+ svc.github_user = creds['username_or_email']
+ svc.github_token = creds['api_token']
+ svc.repo = metadata.get('SearchRepoInput', '')
+ svc.search_query = metadata.get('SearchQueryInput', '')
+ svc.search_issues()
+
+ return svc.result
+
+
+def get_github_issue_details(metadata):
+ """Fetch detailed GitHub issue data for Screen 3 population.
+
+ :param dict metadata: expects key issue_number; repo loaded from DB
+ :return: single-row dict for x0 FormfieldList population
+ """
+ logger.debug('user_routing.get_github_issue_details() metadata:{}'.format(metadata))
+
+ with pool.Handler('x0') as db:
+ creds = _load_credentials(db.connection, 'github')
+
+ if not creds:
+ return {0: {'error': 'GitHub credentials not configured.'}}
+
+ # Repo may be stored as a mapping state or passed via global var.
+ # Fall back to a stored repo config entry.
+ repo = metadata.get('repo', '')
+ if not repo:
+ with pool.Handler('x0') as db:
+ try:
+ with db.connection.cursor() as crs:
+ crs.execute(
+ """
+ SELECT value FROM github2sf.app_config
+ WHERE config_key = 'last_search_repo'
+ """
+ )
+ row = crs.fetchone()
+ if row:
+ repo = row[0]
+ except Exception:
+ pass
+
+ svc = GitHubService()
+ svc.github_user = creds['username_or_email']
+ svc.github_token = creds['api_token']
+ svc.repo = repo
+ svc.issue_number = metadata.get('issue_number', '')
+ svc.get_issue_details()
+
+ return svc.result
+
+
+def create_stackfield_task(metadata):
+ """Create a new Stackfield task from GitHub issue data.
+
+ :param dict metadata: expects task fields and Stackfield room ID
+ :return: result dict with success flag and new task URL
+ """
+ logger.debug('user_routing.create_stackfield_task() metadata:{}'.format(metadata))
+
+ with pool.Handler('x0') as db:
+ creds = _load_credentials(db.connection, 'stackfield')
+
+ if not creds:
+ return {'success': False, 'error': 'Stackfield credentials not configured.'}
+
+ svc = StackfieldService()
+ svc.stackfield_email = creds['username_or_email']
+ svc.stackfield_token = creds['api_token']
+ svc.room_id = metadata.get('StackfieldRoomInput', '')
+ svc.task_title = metadata.get('TaskTitleInput', '')
+ svc.task_description = metadata.get('TaskDescInput', '')
+ svc.task_priority = metadata.get('TaskPriorityPulldown', 'medium')
+ svc.github_issue_number = metadata.get('number', '')
+ svc.github_issue_url = metadata.get('html_url', '')
+ svc.create_task()
+
+ return svc.result
diff --git a/static/menu.json b/static/menu.json
new file mode 100644
index 0000000..dc304d5
--- /dev/null
+++ b/static/menu.json
@@ -0,0 +1,20 @@
+[
+ {
+ "MenuLink1":
+ {
+ "RefID": "sysMenu"
+ }
+ },
+ {
+ "MenuLink2":
+ {
+ "RefID": "sysMenu"
+ }
+ },
+ {
+ "MenuLink3":
+ {
+ "RefID": "sysMenu"
+ }
+ }
+]
diff --git a/static/object.json b/static/object.json
new file mode 100644
index 0000000..e11a243
--- /dev/null
+++ b/static/object.json
@@ -0,0 +1,771 @@
+{
+ "MenuLink1":
+ {
+ "Type": "Link",
+ "Attributes":
+ {
+ "Style": "btn btn-outline-secondary me-2",
+ "TextID": "TXT.MENU.SCREEN1",
+ "ScreenID": "Screen1"
+ }
+ },
+
+ "MenuLink2":
+ {
+ "Type": "Link",
+ "Attributes":
+ {
+ "Style": "btn btn-outline-secondary me-2",
+ "TextID": "TXT.MENU.SCREEN2",
+ "ScreenID": "Screen2"
+ }
+ },
+
+ "MenuLink3":
+ {
+ "Type": "Link",
+ "Attributes":
+ {
+ "Style": "btn btn-outline-secondary me-2",
+ "TextID": "TXT.MENU.SCREEN3",
+ "ScreenID": "Screen3"
+ }
+ },
+
+ "GitHubCredentialsForm":
+ {
+ "Type": "FormfieldList",
+ "Attributes":
+ {
+ "Sections":
+ [
+ {
+ "ID": "GitHubSection",
+ "Object": "FormSectionHeader",
+ "ObjectAttributes":
+ {
+ "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4",
+ "SubStyle": "card-body mt-n6",
+ "HeaderIcon": "fa-brands fa-github",
+ "HeaderTextID": "TXT.SCREEN1.GITHUB.SECTION.HEADER",
+ "SubHeaderTextID": "TXT.SCREEN1.GITHUB.SECTION.SUBHEADER"
+ },
+ "Formfields":
+ [
+ "GitHubUserLabel",
+ "GitHubUserInput",
+ "GitHubTokenLabel",
+ "GitHubTokenInput"
+ ],
+ "RowStyle": "row mb-3",
+ "RowAfterElements": 4,
+ "ColStyle": "col-md-6",
+ "ColAfterElements": 2
+ }
+ ]
+ }
+ },
+
+ "GitHubUserLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN1.GITHUB.USER.LABEL"
+ }
+ },
+
+ "GitHubUserInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "github-username",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "GitHubTokenLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN1.GITHUB.TOKEN.LABEL"
+ }
+ },
+
+ "GitHubTokenInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "password",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "GitHubVerifyButton":
+ {
+ "Type": "Button",
+ "Attributes":
+ {
+ "TextID": "TXT.SCREEN1.GITHUB.VERIFY.BUTTON",
+ "Style": "btn btn-outline-primary w-100 mb-4",
+ "IconStyle": "fa-solid fa-shield-halved",
+ "OnClick": "/python/VerifyGitHubCredentials.py",
+ "SrcDataObjects":
+ [
+ "GitHubCredentialsForm"
+ ],
+ "Notify":
+ {
+ "ID": "VerifyGitHubCredentials",
+ "DisplayHeaderID": "TXT.SCREEN1.GITHUB.VERIFY.NOTIFY"
+ }
+ }
+ },
+
+ "StackfieldCredentialsForm":
+ {
+ "Type": "FormfieldList",
+ "Attributes":
+ {
+ "Sections":
+ [
+ {
+ "ID": "StackfieldSection",
+ "Object": "FormSectionHeader",
+ "ObjectAttributes":
+ {
+ "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4",
+ "SubStyle": "card-body mt-n6",
+ "HeaderIcon": "fa-solid fa-layer-group",
+ "HeaderTextID": "TXT.SCREEN1.STACKFIELD.SECTION.HEADER",
+ "SubHeaderTextID": "TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER"
+ },
+ "Formfields":
+ [
+ "StackfieldEmailLabel",
+ "StackfieldEmailInput",
+ "StackfieldTokenLabel",
+ "StackfieldTokenInput"
+ ],
+ "RowStyle": "row mb-3",
+ "RowAfterElements": 4,
+ "ColStyle": "col-md-6",
+ "ColAfterElements": 2
+ }
+ ]
+ }
+ },
+
+ "StackfieldEmailLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN1.STACKFIELD.EMAIL.LABEL"
+ }
+ },
+
+ "StackfieldEmailInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "user@example.com",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "StackfieldTokenLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN1.STACKFIELD.TOKEN.LABEL"
+ }
+ },
+
+ "StackfieldTokenInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "password",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "stackfield-api-token",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "StackfieldVerifyButton":
+ {
+ "Type": "Button",
+ "Attributes":
+ {
+ "TextID": "TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON",
+ "Style": "btn btn-outline-success w-100 mb-4",
+ "IconStyle": "fa-solid fa-shield-halved",
+ "OnClick": "/python/VerifyStackfieldCredentials.py",
+ "SrcDataObjects":
+ [
+ "StackfieldCredentialsForm"
+ ],
+ "Notify":
+ {
+ "ID": "VerifyStackfieldCredentials",
+ "DisplayHeaderID": "TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY"
+ }
+ }
+ },
+
+ "SearchForm":
+ {
+ "Type": "FormfieldList",
+ "Attributes":
+ {
+ "Sections":
+ [
+ {
+ "ID": "SearchSection",
+ "Object": "FormSectionHeader",
+ "ObjectAttributes":
+ {
+ "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-3",
+ "SubStyle": "card-body mt-n6",
+ "HeaderIcon": "fa-solid fa-magnifying-glass",
+ "HeaderTextID": "TXT.SCREEN2.SEARCH.SECTION.HEADER",
+ "SubHeaderTextID": "TXT.SCREEN2.SEARCH.SECTION.SUBHEADER"
+ },
+ "Formfields":
+ [
+ "SearchQueryLabel",
+ "SearchQueryInput",
+ "SearchRepoLabel",
+ "SearchRepoInput"
+ ],
+ "RowStyle": "row mb-3",
+ "RowAfterElements": 4,
+ "ColStyle": "col-md-6",
+ "ColAfterElements": 2
+ }
+ ]
+ }
+ },
+
+ "SearchQueryLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN2.SEARCH.QUERY.LABEL"
+ }
+ },
+
+ "SearchQueryInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "Search GitHub issues...",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": true
+ }
+ },
+
+ "SearchRepoLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN2.SEARCH.REPO.LABEL"
+ }
+ },
+
+ "SearchRepoInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "Style": "form-control form-control-lg",
+ "Placeholder": "owner/repository",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "SearchButton":
+ {
+ "Type": "Button",
+ "Attributes":
+ {
+ "TextID": "TXT.SCREEN2.SEARCH.BUTTON",
+ "Style": "btn btn-primary w-100 mb-4",
+ "IconStyle": "fa-solid fa-magnifying-glass",
+ "FireEvents": [ "SearchGitHubIssues" ]
+ }
+ },
+
+ "IssueSearchConnector":
+ {
+ "Type": "ServiceConnector",
+ "Attributes":
+ {
+ "OnEvent":
+ {
+ "Events": [ "SearchGitHubIssues" ],
+ "ServiceCall": "/python/SearchGitHubIssues.py"
+ },
+ "SrcDataObjects":
+ {
+ "SearchForm":
+ {
+ "Type": "FormfieldList"
+ }
+ }
+ }
+ },
+
+ "IssueList":
+ {
+ "Type": "List",
+ "Attributes":
+ {
+ "Style": "border rounded-top rounded-bottom",
+ "HeaderRowStyle": "row fw-bold fs-6 m-2 border-bottom bg-light",
+ "RowCount": 15,
+ "Navigation": true,
+ "Columns":
+ [
+ {
+ "ID": "issue_number",
+ "VisibleState": "hidden"
+ },
+ {
+ "ID": "number",
+ "HeaderTextID": "TXT.SCREEN2.LIST.COL.NUMBER",
+ "HeaderStyle": "col-md-1 p-3 border-end text-center"
+ },
+ {
+ "ID": "title",
+ "HeaderTextID": "TXT.SCREEN2.LIST.COL.TITLE",
+ "HeaderStyle": "col-md-5 p-3 border-end"
+ },
+ {
+ "ID": "state",
+ "HeaderTextID": "TXT.SCREEN2.LIST.COL.STATE",
+ "HeaderStyle": "col-md-2 p-3 border-end text-center"
+ },
+ {
+ "ID": "created_at",
+ "HeaderTextID": "TXT.SCREEN2.LIST.COL.CREATED",
+ "HeaderStyle": "col-md-2 p-3 border-end"
+ },
+ {
+ "ID": "assignee",
+ "HeaderTextID": "TXT.SCREEN2.LIST.COL.ASSIGNEE",
+ "HeaderStyle": "col-md-2 p-3"
+ }
+ ],
+ "ContextMenuItems":
+ [
+ {
+ "ID": "ConnectStackfieldTask",
+ "TextID": "TXT.SCREEN2.CONTEXTMENU.CONNECT",
+ "IconStyle": "fa-solid fa-link",
+ "DstScreenID": "Screen3",
+ "RowColumn": "issue_number",
+ "FireEvents": [ "ConnectStackfieldTask" ]
+ }
+ ],
+ "RowStyle": "row m-2 border-bottom",
+ "RowAfterElements": 6,
+ "ColStyle":
+ [
+ "dummy",
+ "col-md-1 p-3 border-end text-center fw-bold text-primary",
+ "col-md-5 p-3 border-end",
+ "col-md-2 p-3 border-end text-center",
+ "col-md-2 p-3 border-end text-muted small",
+ "col-md-2 p-3 text-muted small"
+ ]
+ }
+ },
+
+ "IssueDetailsConnector":
+ {
+ "Type": "ServiceConnector",
+ "Attributes":
+ {
+ "OnEvent":
+ {
+ "Events": [ "ConnectStackfieldTask" ],
+ "ServiceCall": "/python/GetGitHubIssueDetails.py"
+ },
+ "SrcDataObjects":
+ {
+ "issue_number":
+ {
+ "Type": "ScreenGlobalVar"
+ }
+ }
+ }
+ },
+
+ "IssueDetailsForm":
+ {
+ "Type": "FormfieldList",
+ "Attributes":
+ {
+ "Sections":
+ [
+ {
+ "ID": "IssueDetailsSection",
+ "Object": "FormSectionHeader",
+ "ObjectAttributes":
+ {
+ "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4",
+ "SubStyle": "card-body mt-n6",
+ "HeaderIcon": "fa-brands fa-github",
+ "HeaderTextID": "TXT.SCREEN3.ISSUE.SECTION.HEADER",
+ "SubHeaderTextID": "TXT.SCREEN3.ISSUE.SECTION.SUBHEADER"
+ },
+ "Formfields":
+ [
+ "IssueNumberLabel",
+ "IssueNumberField",
+ "IssueStateLabel",
+ "IssueStateField",
+ "IssueTitleLabel",
+ "IssueTitleField",
+ "IssueUrlLabel",
+ "IssueUrlField"
+ ],
+ "RowStyle":
+ [
+ "row mb-3",
+ "row mb-3"
+ ],
+ "RowAfterElements": [ 4, 4 ],
+ "ColStyle":
+ [
+ "col-md-3 mb-2",
+ "col-md-3 mb-2",
+ "col-md-6 mb-2",
+ "col-md-6 mb-2"
+ ]
+ }
+ ],
+ "SrcDataObjects":
+ {
+ "issue_number":
+ {
+ "Type": "ScreenGlobalVar"
+ }
+ }
+ }
+ },
+
+ "IssueNumberLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.ISSUE.NUMBER.LABEL"
+ }
+ },
+
+ "IssueNumberField":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "number",
+ "Style": "form-control",
+ "Disabled": true
+ }
+ },
+
+ "IssueStateLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.ISSUE.STATE.LABEL"
+ }
+ },
+
+ "IssueStateField":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "state",
+ "Style": "form-control",
+ "Disabled": true
+ }
+ },
+
+ "IssueTitleLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.ISSUE.TITLE.LABEL"
+ }
+ },
+
+ "IssueTitleField":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "title",
+ "Style": "form-control",
+ "Disabled": true
+ }
+ },
+
+ "IssueUrlLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.ISSUE.URL.LABEL"
+ }
+ },
+
+ "IssueUrlField":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "html_url",
+ "Style": "form-control",
+ "Disabled": true
+ }
+ },
+
+ "StackfieldMappingForm":
+ {
+ "Type": "FormfieldList",
+ "Attributes":
+ {
+ "Sections":
+ [
+ {
+ "ID": "StackfieldMappingSection",
+ "Object": "FormSectionHeader",
+ "ObjectAttributes":
+ {
+ "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4",
+ "SubStyle": "card-body mt-n6",
+ "HeaderIcon": "fa-solid fa-layer-group",
+ "HeaderTextID": "TXT.SCREEN3.STACKFIELD.SECTION.HEADER",
+ "SubHeaderTextID": "TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER"
+ },
+ "Formfields":
+ [
+ "StackfieldRoomLabel",
+ "StackfieldRoomInput",
+ "TaskTitleLabel",
+ "TaskTitleInput",
+ "TaskDescLabel",
+ "TaskDescInput",
+ "TaskPriorityLabel",
+ "TaskPriorityPulldown"
+ ],
+ "RowStyle":
+ [
+ "row mb-3",
+ "row mb-3",
+ "row mb-3"
+ ],
+ "RowAfterElements": [ 4, 2, 2 ],
+ "ColStyle":
+ [
+ "col-md-6 mb-2",
+ "col-md-6 mb-2",
+ "col-md-12 mb-2",
+ "col-md-12 mb-2"
+ ]
+ }
+ ]
+ }
+ },
+
+ "StackfieldRoomLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.STACKFIELD.ROOM.LABEL"
+ }
+ },
+
+ "StackfieldRoomInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "stackfield_room_id",
+ "Style": "form-control",
+ "Placeholder": "Stackfield Room ID",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "TaskTitleLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.TASK.TITLE.LABEL"
+ }
+ },
+
+ "TaskTitleInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "text",
+ "DBColumn": "task_title",
+ "Style": "form-control",
+ "ValidateRef": "DefaultString",
+ "ValidateNullable": false
+ }
+ },
+
+ "TaskDescLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.TASK.DESC.LABEL"
+ }
+ },
+
+ "TaskDescInput":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "textarea",
+ "DBColumn": "task_description",
+ "Style": "form-control",
+ "Rows": 5,
+ "ValidateNullable": true
+ }
+ },
+
+ "TaskPriorityLabel":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "label",
+ "Style": "form-label fw-semibold",
+ "TextID": "TXT.SCREEN3.TASK.PRIORITY.LABEL"
+ }
+ },
+
+ "TaskPriorityPulldown":
+ {
+ "Type": "Formfield",
+ "Attributes":
+ {
+ "Type": "pulldown",
+ "DBColumn": "task_priority",
+ "Style": "form-select",
+ "Options":
+ [
+ {
+ "TextID": "TXT.SCREEN3.PRIORITY.LOW",
+ "Value": "low",
+ "Default": true
+ },
+ {
+ "TextID": "TXT.SCREEN3.PRIORITY.MEDIUM",
+ "Value": "medium"
+ },
+ {
+ "TextID": "TXT.SCREEN3.PRIORITY.HIGH",
+ "Value": "high"
+ },
+ {
+ "TextID": "TXT.SCREEN3.PRIORITY.URGENT",
+ "Value": "urgent"
+ }
+ ]
+ }
+ },
+
+ "CreateStackfieldTaskButton":
+ {
+ "Type": "Button",
+ "Attributes":
+ {
+ "TextID": "TXT.SCREEN3.CREATE.BUTTON",
+ "Style": "btn btn-success w-100 mt-2 mb-4",
+ "IconStyle": "fa-solid fa-circle-plus",
+ "OnClick": "/python/CreateStackfieldTask.py",
+ "SrcDataObjects":
+ [
+ "IssueDetailsForm",
+ "StackfieldMappingForm"
+ ],
+ "Notify":
+ {
+ "ID": "CreateStackfieldTask",
+ "DisplayHeaderID": "TXT.SCREEN3.CREATE.NOTIFY"
+ }
+ }
+ }
+}
diff --git a/static/skeleton.json b/static/skeleton.json
new file mode 100644
index 0000000..060be60
--- /dev/null
+++ b/static/skeleton.json
@@ -0,0 +1,85 @@
+{
+ "Screen1":
+ [
+ {
+ "GitHubCredentialsForm":
+ {
+ "RefID": "Screen1"
+ }
+ },
+ {
+ "GitHubVerifyButton":
+ {
+ "RefID": "Screen1"
+ }
+ },
+ {
+ "StackfieldCredentialsForm":
+ {
+ "RefID": "Screen1"
+ }
+ },
+ {
+ "StackfieldVerifyButton":
+ {
+ "RefID": "Screen1"
+ }
+ }
+ ],
+
+ "Screen2":
+ [
+ {
+ "SearchForm":
+ {
+ "RefID": "Screen2"
+ }
+ },
+ {
+ "SearchButton":
+ {
+ "RefID": "Screen2"
+ }
+ },
+ {
+ "IssueSearchConnector":
+ {
+ "RefID": "Screen2"
+ }
+ },
+ {
+ "IssueList":
+ {
+ "RefID": "IssueSearchConnector"
+ }
+ }
+ ],
+
+ "Screen3":
+ [
+ {
+ "IssueDetailsConnector":
+ {
+ "RefID": "Screen3"
+ }
+ },
+ {
+ "IssueDetailsForm":
+ {
+ "RefID": "IssueDetailsConnector"
+ }
+ },
+ {
+ "StackfieldMappingForm":
+ {
+ "RefID": "IssueDetailsConnector"
+ }
+ },
+ {
+ "CreateStackfieldTaskButton":
+ {
+ "RefID": "Screen3"
+ }
+ }
+ ]
+}
From a90ef1317ca578bb2bd8c19e9c8cd67420a6c645 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 09:13:10 +0000
Subject: [PATCH 2/6] fix: use official ghcr.io/webcodex1/x0-app and x0-db
container images
---
README.md | 23 ++++++++++++-------
docker/Dockerfile | 30 +++++-------------------
docker/apache2.conf | 48 ++++++++++++---------------------------
docker/docker-compose.yml | 45 ++++++++++++++++++------------------
4 files changed, 58 insertions(+), 88 deletions(-)
diff --git a/README.md b/README.md
index 6698eb5..a7c0ad8 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ psql -U postgres -d x0 -f database/03-insert-text.sql
Copy the `static/` directory so it is served at `/static/github2sf/`:
```bash
-cp -r static/ /var/www/x0/static/github2sf/
+cp -r static/ /var/www/vhosts/x0/static/github2sf/
```
### 5. Deploy Python backend
@@ -94,14 +94,15 @@ cp -r static/ /var/www/x0/static/github2sf/
Copy the `python/` directory into the x0 Python directory:
```bash
-cp python/*.py /var/www/x0/python/github2sf/
+cp python/*.py /var/www/vhosts/x0/python/github2sf/
```
### 6. Configure Apache2
-Add the WSGI aliases from `docker/apache2.conf` to your Apache virtual host, then reload:
+Copy `docker/apache2.conf` to `/etc/apache2/conf-enabled/github2sf.conf` and reload:
```bash
+cp docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf
apache2ctl graceful
```
@@ -113,6 +114,13 @@ Navigate to `http://your-server/?appid=github2sf` in your browser.
## Docker (quick start)
+The application uses the official x0 container images:
+
+- **`ghcr.io/webcodex1/x0-app`** — Apache2 + mod_wsgi web application server with the x0 JavaScript framework pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-app))
+- **`ghcr.io/webcodex1/x0-db`** — PostgreSQL 16 database with the x0 schema pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-db))
+
+The `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and adds the github2stackfield Python backend dependencies on top.
+
```bash
cd docker
docker compose up --build
@@ -120,13 +128,12 @@ docker compose up --build
Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf).
-> **Note:** The Docker image fetches x0 and python-micro-esb from GitHub at build time.
-> You still need to run the database SQL scripts against the PostgreSQL container:
+> **Database setup:** After the containers are running, execute the SQL scripts against the x0-db container:
>
> ```bash
-> docker exec -i github2sf-db psql -U postgres -d x0 < database/01-create-schema.sql
-> docker exec -i github2sf-db psql -U postgres -d x0 < database/02-insert-config.sql
-> docker exec -i github2sf-db psql -U postgres -d x0 < database/03-insert-text.sql
+> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/01-create-schema.sql
+> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/02-insert-config.sql
+> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/03-insert-text.sql
> ```
---
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 353e760..0a679e7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,31 +1,13 @@
-FROM python:3.12-slim
+FROM ghcr.io/webcodex1/x0-app
-RUN apt-get update && apt-get install -y \
- apache2 \
- libapache2-mod-wsgi-py3 \
+# Install github2stackfield Python backend dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3-pip \
git \
&& rm -rf /var/lib/apt/lists/*
-# Install Python dependencies
-RUN pip install --no-cache-dir \
+RUN pip3 install --no-cache-dir --break-system-packages \
requests \
psycopg2-binary \
- pgdbpool
-
-# Install python-micro-esb from source
-RUN pip install --no-cache-dir \
+ pgdbpool \
git+https://github.com/clauspruefer/python-micro-esb.git
-
-# Install x0 framework Python modules
-RUN pip install --no-cache-dir \
- git+https://github.com/WEBcodeX1/x0.git
-
-WORKDIR /var/www/x0
-
-# Apache configuration
-COPY docker/apache2.conf /etc/apache2/sites-available/github2sf.conf
-RUN a2ensite github2sf && a2dissite 000-default && a2enmod wsgi
-
-EXPOSE 80
-
-CMD ["apache2ctl", "-D", "FOREGROUND"]
diff --git a/docker/apache2.conf b/docker/apache2.conf
index 61ebf99..b0f3854 100644
--- a/docker/apache2.conf
+++ b/docker/apache2.conf
@@ -1,35 +1,17 @@
-
- ServerName github2sf.local
- DocumentRoot /var/www/x0/www
+# github2stackfield supplementary Apache configuration
+# Mounted into /etc/apache2/conf-enabled/ inside the x0-app container.
+# The x0-app base image already configures Apache2 + mod_wsgi and sets
+# DocumentRoot /var/www/vhosts/x0 with WSGI handling for the python/ tree.
+# This snippet grants explicit directory access to the app-specific paths.
- # Static assets: x0 framework JS/CSS + app-specific JSON config
- Alias /static /var/www/x0/www/static
- Alias /static/github2sf /var/www/x0/static/github2sf
+
+ Options -Indexes
+ Require all granted
+
-
- Options -Indexes
- Require all granted
-
-
-
- Options -Indexes
- Require all granted
-
-
- # x0 framework index / main entry-point
- WSGIScriptAlias / /var/www/x0/python/Index.py
-
- # github2stackfield backend service endpoints
- WSGIScriptAlias /python/VerifyGitHubCredentials.py /var/www/x0/python/github2sf/VerifyGitHubCredentials.py
- WSGIScriptAlias /python/VerifyStackfieldCredentials.py /var/www/x0/python/github2sf/VerifyStackfieldCredentials.py
- WSGIScriptAlias /python/SearchGitHubIssues.py /var/www/x0/python/github2sf/SearchGitHubIssues.py
- WSGIScriptAlias /python/GetGitHubIssueDetails.py /var/www/x0/python/github2sf/GetGitHubIssueDetails.py
- WSGIScriptAlias /python/CreateStackfieldTask.py /var/www/x0/python/github2sf/CreateStackfieldTask.py
-
-
- Require all granted
-
-
- ErrorLog ${APACHE_LOG_DIR}/github2sf-error.log
- CustomLog ${APACHE_LOG_DIR}/github2sf-access.log combined
-
+
+ AddHandler wsgi-script .py
+ Options ExecCGI
+ AllowOverride None
+ Require all granted
+
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 76f5829..d58a2ae 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,25 +1,24 @@
services:
# ---------------------------------------------------------------------------
- # PostgreSQL database (shared with x0 framework)
+ # x0 database container (PostgreSQL with x0 schema pre-installed)
+ # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db
# ---------------------------------------------------------------------------
- postgres:
- image: postgres:16-alpine
+ x0-db:
+ image: ghcr.io/webcodex1/x0-db
container_name: github2sf-db
restart: unless-stopped
- environment:
- POSTGRES_DB: x0
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: changeme
- volumes:
- - pg_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- - github2sf-net
+ github2sf-net:
+ ipv4_address: 172.20.0.20
+ aliases:
+ - mypostgres
# ---------------------------------------------------------------------------
- # x0 application server (Apache2 + mod_wsgi)
+ # x0 application server (extends official x0-app with github2sf Python deps)
+ # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app
# ---------------------------------------------------------------------------
x0-app:
build:
@@ -28,23 +27,23 @@ services:
container_name: github2sf-app
restart: unless-stopped
depends_on:
- - postgres
- environment:
- DB_HOST: postgres
- DB_NAME: x0
- DB_USER: postgres
- DB_PASSWORD: changeme
+ - x0-db
+ extra_hosts:
+ - "mypostgres:172.20.0.20"
volumes:
- - ../static:/var/www/x0/static/github2sf:ro
- - ../python:/var/www/x0/python/github2sf:ro
+ - ../static:/var/www/vhosts/x0/static/github2sf:ro
+ - ../python:/var/www/vhosts/x0/python/github2sf:ro
+ - ../docker/apache2.conf:/etc/apache2/conf-enabled/github2sf.conf:ro
ports:
- "8080:80"
networks:
- - github2sf-net
-
-volumes:
- pg_data:
+ github2sf-net:
+ ipv4_address: 172.20.0.10
networks:
github2sf-net:
driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/24
+ gateway: 172.20.0.1
From 0d8ba8a2db1eddd0511e0e1ad930c0a0c56e65fa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 10:14:40 +0000
Subject: [PATCH 3/6] fix: automate full container self-initialisation; remove
## Installation from README
---
README.md | 75 ++++---------------------------
database/03-insert-text.sql | 88 ++++++++++++++++++-------------------
docker/Dockerfile | 9 ++++
docker/Dockerfile.db | 11 +++++
docker/db-init.sh | 35 +++++++++++++++
docker/docker-compose.yml | 18 ++++----
6 files changed, 117 insertions(+), 119 deletions(-)
create mode 100644 docker/Dockerfile.db
create mode 100755 docker/db-init.sh
diff --git a/README.md b/README.md
index a7c0ad8..98b7b57 100644
--- a/README.md
+++ b/README.md
@@ -50,68 +50,12 @@ Review the selected GitHub issue and create a Stackfield task.
| Requirement | Notes |
|---|---|
-| x0 framework | Follow [x0 INSTALL.md](https://github.com/WEBcodeX1/x0/blob/main/INSTALL.md) |
-| PostgreSQL ≥ 14 | Shared with x0 |
-| Python ≥ 3.10 | `requests`, `psycopg2`, `pgdbpool`, `python-micro-esb` |
+| Docker Engine | With Compose V2 (`docker compose`) |
| GitHub Personal Access Token | Needs `repo` scope for private repos, `public_repo` for public |
| Stackfield API token | See Stackfield workspace settings → Integrations → API |
---
-## Installation
-
-### 1. Set up x0
-
-Follow the official x0 installation guide to get the base framework running with PostgreSQL.
-
-### 2. Install Python dependencies
-
-```bash
-pip install requests psycopg2-binary pgdbpool
-pip install git+https://github.com/clauspruefer/python-micro-esb.git
-```
-
-### 3. Run database setup scripts
-
-Connect to your x0 PostgreSQL database and execute the scripts in order:
-
-```bash
-psql -U postgres -d x0 -f database/01-create-schema.sql
-psql -U postgres -d x0 -f database/02-insert-config.sql
-psql -U postgres -d x0 -f database/03-insert-text.sql
-```
-
-### 4. Deploy static files
-
-Copy the `static/` directory so it is served at `/static/github2sf/`:
-
-```bash
-cp -r static/ /var/www/vhosts/x0/static/github2sf/
-```
-
-### 5. Deploy Python backend
-
-Copy the `python/` directory into the x0 Python directory:
-
-```bash
-cp python/*.py /var/www/vhosts/x0/python/github2sf/
-```
-
-### 6. Configure Apache2
-
-Copy `docker/apache2.conf` to `/etc/apache2/conf-enabled/github2sf.conf` and reload:
-
-```bash
-cp docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf
-apache2ctl graceful
-```
-
-### 7. Open the application
-
-Navigate to `http://your-server/?appid=github2sf` in your browser.
-
----
-
## Docker (quick start)
The application uses the official x0 container images:
@@ -119,7 +63,10 @@ The application uses the official x0 container images:
- **`ghcr.io/webcodex1/x0-app`** — Apache2 + mod_wsgi web application server with the x0 JavaScript framework pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-app))
- **`ghcr.io/webcodex1/x0-db`** — PostgreSQL 16 database with the x0 schema pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-db))
-The `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and adds the github2stackfield Python backend dependencies on top.
+Both custom images are built automatically on `docker compose up --build`:
+
+- `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and copies all static files, Python WSGI scripts, and the Apache2 config snippet into the image.
+- `docker/Dockerfile.db` extends `ghcr.io/webcodex1/x0-db` and runs the github2stackfield SQL init scripts (`database/0*.sql`) against the x0 database on first start.
```bash
cd docker
@@ -128,13 +75,7 @@ docker compose up --build
Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf).
-> **Database setup:** After the containers are running, execute the SQL scripts against the x0-db container:
->
-> ```bash
-> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/01-create-schema.sql
-> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/02-insert-config.sql
-> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/03-insert-text.sql
-> ```
+No further manual configuration is required.
---
@@ -161,8 +102,10 @@ github2stackfield/
│ ├── 02-insert-config.sql # x0 app configuration rows
│ └── 03-insert-text.sql # UI text / i18n entries
├── docker/
-│ ├── Dockerfile
+│ ├── Dockerfile # extends x0-app, bakes in static/ python/ apache2.conf
+│ ├── Dockerfile.db # extends x0-db, auto-inits github2sf schema on startup
│ ├── docker-compose.yml
+│ ├── db-init.sh # startup script used by Dockerfile.db
│ └── apache2.conf
└── README.md
```
diff --git a/database/03-insert-text.sql b/database/03-insert-text.sql
index 0f4aaa8..3235890 100644
--- a/database/03-insert-text.sql
+++ b/database/03-insert-text.sql
@@ -9,108 +9,108 @@
-- Navigation / Menu
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials');
+ ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping');
+ ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task');
+ ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task') ON CONFLICT (id) DO NOTHING;
-- Screen 1 – GitHub Credentials
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials');
+ ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token');
+ ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username');
+ ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token');
+ ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials');
+ ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication');
+ ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication') ON CONFLICT (id) DO NOTHING;
-- Screen 1 – Stackfield Credentials
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials');
+ ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token');
+ ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email');
+ ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token');
+ ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials');
+ ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication');
+ ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication') ON CONFLICT (id) DO NOTHING;
-- Screen 2 – Search
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues');
+ ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term');
+ ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)');
+ ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)');
+ ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues');
+ ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues') ON CONFLICT (id) DO NOTHING;
-- Screen 2 – Issue List columns
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#');
+ ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title');
+ ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State');
+ ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created');
+ ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee');
+ ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee') ON CONFLICT (id) DO NOTHING;
-- Screen 2 – Context menu
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task');
+ ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task') ON CONFLICT (id) DO NOTHING;
-- Screen 3 – GitHub Issue Details
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties');
+ ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API');
+ ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number');
+ ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State');
+ ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title');
+ ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL');
+ ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL') ON CONFLICT (id) DO NOTHING;
-- Screen 3 – Stackfield Mapping
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping');
+ ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details');
+ ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID');
+ ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title');
+ ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description');
+ ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority');
+ ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority') ON CONFLICT (id) DO NOTHING;
-- Screen 3 – Priority options
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low');
+ ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium');
+ ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High');
+ ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent');
+ ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent') ON CONFLICT (id) DO NOTHING;
-- Screen 3 – Create button
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task');
+ ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task') ON CONFLICT (id) DO NOTHING;
INSERT INTO webui.text (id, "group", value_de, value_en) VALUES
- ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task');
+ ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task') ON CONFLICT (id) DO NOTHING;
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 0a679e7..0918fc4 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -11,3 +11,12 @@ RUN pip3 install --no-cache-dir --break-system-packages \
psycopg2-binary \
pgdbpool \
git+https://github.com/clauspruefer/python-micro-esb.git
+
+# Copy static x0 config files into the web-server document root
+COPY static/ /var/www/vhosts/x0/static/github2sf/
+
+# Copy Python WSGI backend scripts into the x0 python directory
+COPY python/ /var/www/vhosts/x0/python/github2sf/
+
+# Install the supplementary Apache2 configuration snippet
+COPY docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf
diff --git a/docker/Dockerfile.db b/docker/Dockerfile.db
new file mode 100644
index 0000000..83bea28
--- /dev/null
+++ b/docker/Dockerfile.db
@@ -0,0 +1,11 @@
+FROM ghcr.io/webcodex1/x0-db
+
+# Copy the github2stackfield SQL init scripts into the container
+COPY database/ /opt/github2sf-init/
+
+# Copy the custom startup script and make it executable
+COPY docker/db-init.sh /root/github2sf-start.sh
+RUN chmod +x /root/github2sf-start.sh
+
+# Override the default x0-db startup with our initialising script
+CMD ["/root/github2sf-start.sh"]
diff --git a/docker/db-init.sh b/docker/db-init.sh
new file mode 100755
index 0000000..02172e2
--- /dev/null
+++ b/docker/db-init.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# ]*[ --------------------------------------------------------------------- ]*[
+# . github2stackfield - x0-db automatic database initialisation .
+# . .
+# . Replaces the default x0-db start script. .
+# . Starts PostgreSQL, waits until it accepts connections, applies the .
+# . github2stackfield SQL scripts against the x0 database, then keeps .
+# . the server running. .
+# ]*[ --------------------------------------------------------------------- ]*[
+set -e
+
+chown -R postgres:postgres /var/lib/postgresql/16/main/
+chown -R postgres:postgres /var/run/postgresql/
+
+# Start PostgreSQL in the background
+su -c "/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main" postgres &
+PG_PID=$!
+
+# Wait until PostgreSQL is ready to accept connections
+echo "Waiting for PostgreSQL to be ready..."
+until su -c "pg_isready -q -U postgres -d x0" postgres 2>/dev/null; do
+ sleep 1
+done
+echo "PostgreSQL is ready."
+
+# Apply github2stackfield database init scripts to the x0 database.
+# All scripts are idempotent and safe to re-run on container restart.
+echo "Applying github2stackfield database init scripts..."
+su -c "psql -d x0 -f /opt/github2sf-init/01-create-schema.sql" postgres
+su -c "psql -d x0 -f /opt/github2sf-init/02-insert-config.sql" postgres
+su -c "psql -d x0 -f /opt/github2sf-init/03-insert-text.sql" postgres
+echo "Database initialisation complete."
+
+# Keep the container alive by waiting for the PostgreSQL process
+wait $PG_PID
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index d58a2ae..bb942dc 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,11 +1,14 @@
services:
# ---------------------------------------------------------------------------
- # x0 database container (PostgreSQL with x0 schema pre-installed)
- # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db
+ # x0 database container — extends the official x0-db image and
+ # automatically initialises the github2stackfield schema + data on startup.
+ # Official base: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db
# ---------------------------------------------------------------------------
x0-db:
- image: ghcr.io/webcodex1/x0-db
+ build:
+ context: ..
+ dockerfile: docker/Dockerfile.db
container_name: github2sf-db
restart: unless-stopped
ports:
@@ -17,8 +20,9 @@ services:
- mypostgres
# ---------------------------------------------------------------------------
- # x0 application server (extends official x0-app with github2sf Python deps)
- # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app
+ # x0 application server — extends the official x0-app image and
+ # bundles all static / Python backend / Apache config at build time.
+ # Official base: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app
# ---------------------------------------------------------------------------
x0-app:
build:
@@ -30,10 +34,6 @@ services:
- x0-db
extra_hosts:
- "mypostgres:172.20.0.20"
- volumes:
- - ../static:/var/www/vhosts/x0/static/github2sf:ro
- - ../python:/var/www/vhosts/x0/python/github2sf:ro
- - ../docker/apache2.conf:/etc/apache2/conf-enabled/github2sf.conf:ro
ports:
- "8080:80"
networks:
From b878e52a76d6cff93715a5fb89e7d4ff2b3204ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Claus=20Pr=C3=BCfer?=
Date: Wed, 27 May 2026 12:25:19 +0200
Subject: [PATCH 4/6] Add initial AI prompt
---
AI-prompt.md | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 AI-prompt.md
diff --git a/AI-prompt.md b/AI-prompt.md
new file mode 100644
index 0000000..23bec37
--- /dev/null
+++ b/AI-prompt.md
@@ -0,0 +1,16 @@
+Build an x0 application using the x0 framework from https://github.com/WEBcodeX1/x0,
+The backend in Python should use the https://github.com/clauspruefer/python-micro-esb project
+The CSS styling is done with the x0 contained Bootstrap default theme and should look modern.
+The browser-frontend consists of 3 x0 screens / 3 menu entries:
+1. Screen1 "User Credentials": contains 2 groups of formfields (x0 object of type `sysObjectFormfieldList`): group a) GitHub API User/Token credentials group b) Stackfield API User/Token Credentials
+1.1. On each formfield group there must be a button "verify user credentials" to test if the API authentication is working
+2. Screen2 "Issue / Task Mapping": contains the mapping between GitHub Issues and Stackfield Tasks
+2.1. On top there should be 1 formfield / search input and a search execution button (x0 object of type `sysObjectFormfieldList`)
+2.2. Under the search must be a list (x0 object of type `sysObjectList` containing the search results
+2.3. The search result list from 2.2. contains a x0 object of type `sysObjContextMenu` with one entry "Connect Stackfield Task"
+2.4. On button click from 2.1 the relevant python-micro-esb backend service must be called and the given result list from 2.2 must be filled with the result data from the backend service
+3. Screen3 "Connect Stackfield Task"
+3.1. Contains detailed GitHub Task properties from GitHub API webservice result (encapsulated from python-micro-esb backend), e.g. 3 or four formfields describing the task properties from API result
+3.2. On right click of screen 2 object result display list single row, the GitHub task properties fields of Screen3 will be updated from the row data
+3.3. Also there should be relevant Stackfield properties where to map the Stackfield Task (Stackfield room or similar, please find the best possible fields from the API to map)
+3.4. At the bottom there must be 1 button (x0 object type `sysObjButton`), on click a NEW STACKFIELD TASK will be generated from the relevant GitHub Issue/Task data from Screen3 formfield data.
From 372a76ddd613799a0d9d84c8d14c885adc222244 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 10:33:52 +0000
Subject: [PATCH 5/6] fix: resolve missing stylesheets and WSGI 404 errors
---
docker/Dockerfile | 7 +++++++
python/CreateStackfieldTask.py | 4 +++-
python/GetGitHubIssueDetails.py | 4 +++-
python/SearchGitHubIssues.py | 4 +++-
python/VerifyGitHubCredentials.py | 4 +++-
python/VerifyStackfieldCredentials.py | 4 +++-
static/object.json | 10 +++++-----
7 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 0918fc4..6707704 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -15,6 +15,13 @@ RUN pip3 install --no-cache-dir --break-system-packages \
# Copy static x0 config files into the web-server document root
COPY static/ /var/www/vhosts/x0/static/github2sf/
+# Link the x0 base stylesheets and fontawesome into the app subdir so that
+# the x0 framework can find bootstrap.css, globalstyles.css and all.min.css
+# at the {subdir}/ path it generates in the HTML tags.
+RUN ln -s /var/www/vhosts/x0/static/bootstrap.css /var/www/vhosts/x0/static/github2sf/bootstrap.css \
+ && ln -s /var/www/vhosts/x0/static/globalstyles.css /var/www/vhosts/x0/static/github2sf/globalstyles.css \
+ && ln -s /var/www/vhosts/x0/static/fontawesome /var/www/vhosts/x0/static/github2sf/fontawesome
+
# Copy Python WSGI backend scripts into the x0 python directory
COPY python/ /var/www/vhosts/x0/python/github2sf/
diff --git a/python/CreateStackfieldTask.py b/python/CreateStackfieldTask.py
index 76513e8..795b8c0 100644
--- a/python/CreateStackfieldTask.py
+++ b/python/CreateStackfieldTask.py
@@ -11,10 +11,12 @@
import sys
import json
+sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf')
+
import POSTData
from StdoutLogger import logger
-from router import ServiceRouter
+from microesb.router import ServiceRouter
def application(environ, start_response):
diff --git a/python/GetGitHubIssueDetails.py b/python/GetGitHubIssueDetails.py
index d4d345f..f008efb 100644
--- a/python/GetGitHubIssueDetails.py
+++ b/python/GetGitHubIssueDetails.py
@@ -11,10 +11,12 @@
import sys
import json
+sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf')
+
import POSTData
from StdoutLogger import logger
-from router import ServiceRouter
+from microesb.router import ServiceRouter
def application(environ, start_response):
diff --git a/python/SearchGitHubIssues.py b/python/SearchGitHubIssues.py
index b67f3d1..f3dd07f 100644
--- a/python/SearchGitHubIssues.py
+++ b/python/SearchGitHubIssues.py
@@ -11,10 +11,12 @@
import sys
import json
+sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf')
+
import POSTData
from StdoutLogger import logger
-from router import ServiceRouter
+from microesb.router import ServiceRouter
def application(environ, start_response):
diff --git a/python/VerifyGitHubCredentials.py b/python/VerifyGitHubCredentials.py
index 4c8f791..8b5121c 100644
--- a/python/VerifyGitHubCredentials.py
+++ b/python/VerifyGitHubCredentials.py
@@ -10,10 +10,12 @@
import sys
import json
+sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf')
+
import POSTData
from StdoutLogger import logger
-from router import ServiceRouter
+from microesb.router import ServiceRouter
def application(environ, start_response):
diff --git a/python/VerifyStackfieldCredentials.py b/python/VerifyStackfieldCredentials.py
index f4206b2..b2b7eb9 100644
--- a/python/VerifyStackfieldCredentials.py
+++ b/python/VerifyStackfieldCredentials.py
@@ -10,10 +10,12 @@
import sys
import json
+sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf')
+
import POSTData
from StdoutLogger import logger
-from router import ServiceRouter
+from microesb.router import ServiceRouter
def application(environ, start_response):
diff --git a/static/object.json b/static/object.json
index e11a243..ae828f7 100644
--- a/static/object.json
+++ b/static/object.json
@@ -122,7 +122,7 @@
"TextID": "TXT.SCREEN1.GITHUB.VERIFY.BUTTON",
"Style": "btn btn-outline-primary w-100 mb-4",
"IconStyle": "fa-solid fa-shield-halved",
- "OnClick": "/python/VerifyGitHubCredentials.py",
+ "OnClick": "/python/github2sf/VerifyGitHubCredentials.py",
"SrcDataObjects":
[
"GitHubCredentialsForm"
@@ -225,7 +225,7 @@
"TextID": "TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON",
"Style": "btn btn-outline-success w-100 mb-4",
"IconStyle": "fa-solid fa-shield-halved",
- "OnClick": "/python/VerifyStackfieldCredentials.py",
+ "OnClick": "/python/github2sf/VerifyStackfieldCredentials.py",
"SrcDataObjects":
[
"StackfieldCredentialsForm"
@@ -340,7 +340,7 @@
"OnEvent":
{
"Events": [ "SearchGitHubIssues" ],
- "ServiceCall": "/python/SearchGitHubIssues.py"
+ "ServiceCall": "/python/github2sf/SearchGitHubIssues.py"
},
"SrcDataObjects":
{
@@ -426,7 +426,7 @@
"OnEvent":
{
"Events": [ "ConnectStackfieldTask" ],
- "ServiceCall": "/python/GetGitHubIssueDetails.py"
+ "ServiceCall": "/python/github2sf/GetGitHubIssueDetails.py"
},
"SrcDataObjects":
{
@@ -755,7 +755,7 @@
"TextID": "TXT.SCREEN3.CREATE.BUTTON",
"Style": "btn btn-success w-100 mt-2 mb-4",
"IconStyle": "fa-solid fa-circle-plus",
- "OnClick": "/python/CreateStackfieldTask.py",
+ "OnClick": "/python/github2sf/CreateStackfieldTask.py",
"SrcDataObjects":
[
"IssueDetailsForm",
From 55f34e51bf269d3da3d51acbb06ceb01152d114c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 27 May 2026 10:48:03 +0000
Subject: [PATCH 6/6] fix: display menu buttons vertically (stacked) instead of
side by side
---
static/object.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/object.json b/static/object.json
index ae828f7..2ec2c91 100644
--- a/static/object.json
+++ b/static/object.json
@@ -4,7 +4,7 @@
"Type": "Link",
"Attributes":
{
- "Style": "btn btn-outline-secondary me-2",
+ "Style": "btn btn-outline-secondary d-block mb-2",
"TextID": "TXT.MENU.SCREEN1",
"ScreenID": "Screen1"
}
@@ -15,7 +15,7 @@
"Type": "Link",
"Attributes":
{
- "Style": "btn btn-outline-secondary me-2",
+ "Style": "btn btn-outline-secondary d-block mb-2",
"TextID": "TXT.MENU.SCREEN2",
"ScreenID": "Screen2"
}
@@ -26,7 +26,7 @@
"Type": "Link",
"Attributes":
{
- "Style": "btn btn-outline-secondary me-2",
+ "Style": "btn btn-outline-secondary d-block mb-2",
"TextID": "TXT.MENU.SCREEN3",
"ScreenID": "Screen3"
}