refactored attributes / timestamps / data objects CRUD

This commit is contained in:
ValueOn AG 2025-08-23 01:16:20 +02:00
parent 4edaba3471
commit 2b5287188d
9 changed files with 479 additions and 108 deletions

View file

@ -31,10 +31,39 @@ class ConnectionStatus(str, Enum):
class Mandate(BaseModel, ModelMixin):
"""Data model for a mandate"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate")
language: str = Field(default="en", description="Default language of the mandate")
enabled: bool = Field(default=True, description="Indicates whether the mandate is enabled")
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
name: str = Field(
description="Name of the mandate",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
language: str = Field(
default="en",
description="Default language of the mandate",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
]
)
enabled: bool = Field(
default=True,
description="Indicates whether the mandate is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
# Register labels for Mandate
register_model_labels(
@ -50,20 +79,83 @@ register_model_labels(
class UserConnection(BaseModel, ModelMixin):
"""Data model for a user's connection to an external service"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
userId: str = Field(description="ID of the user this connection belongs to")
authority: AuthAuthority = Field(description="Authentication authority")
externalId: str = Field(description="User ID in the external system")
externalUsername: str = Field(description="Username in the external system")
externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system")
status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status")
connectedAt: float = Field(default_factory=get_utc_timestamp, description="When the connection was established (UTC timestamp in seconds)")
lastChecked: float = Field(default_factory=get_utc_timestamp, description="When the connection was last verified (UTC timestamp in seconds)")
expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)")
def to_dict(self) -> Dict[str, Any]:
"""Convert the model to a dictionary"""
return super().to_dict()
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the connection",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
userId: str = Field(
description="ID of the user this connection belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
authority: AuthAuthority = Field(
description="Authentication authority",
frontend_type="select",
frontend_readonly=True,
frontend_required=False,
frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
]
)
externalId: str = Field(
description="User ID in the external system",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
externalUsername: str = Field(
description="Username in the external system",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
externalEmail: Optional[EmailStr] = Field(
None,
description="Email in the external system",
frontend_type="email",
frontend_readonly=False,
frontend_required=False
)
status: ConnectionStatus = Field(
default=ConnectionStatus.ACTIVE,
description="Connection status",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
frontend_options=[
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "inactive", "label": {"en": "Inactive", "fr": "Inactif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}}
]
)
connectedAt: float = Field(
default_factory=get_utc_timestamp,
description="When the connection was established (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
lastChecked: float = Field(
default_factory=get_utc_timestamp,
description="When the connection was last verified (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
expiresAt: Optional[float] = Field(
None,
description="When the connection expires (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
# Register labels for UserConnection
register_model_labels(
@ -135,15 +227,84 @@ register_model_labels(
class User(BaseModel, ModelMixin):
"""Data model for a user"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login")
email: Optional[EmailStr] = Field(None, description="Email address of the user")
fullName: Optional[str] = Field(None, description="Full name of the user")
language: str = Field(default="en", description="Preferred language of the user")
enabled: bool = Field(default=True, description="Indicates whether the user is enabled")
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the user",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
username: str = Field(
description="Username for login",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
email: Optional[EmailStr] = Field(
None,
description="Email address of the user",
frontend_type="email",
frontend_readonly=False,
frontend_required=True
)
fullName: Optional[str] = Field(
None,
description="Full name of the user",
frontend_type="text",
frontend_readonly=False,
frontend_required=False
)
language: str = Field(
default="en",
description="Preferred language of the user",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}}
]
)
enabled: bool = Field(
default=True,
description="Indicates whether the user is enabled",
frontend_type="checkbox",
frontend_readonly=False,
frontend_required=False
)
privilege: UserPrivilege = Field(
default=UserPrivilege.USER,
description="Permission level",
frontend_type="select",
frontend_readonly=False,
frontend_required=True,
frontend_options=[
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
{"value": "admin", "label": {"en": "Admin", "fr": "Administrateur"}},
{"value": "sysadmin", "label": {"en": "SysAdmin", "fr": "Administrateur système"}}
]
)
authenticationAuthority: AuthAuthority = Field(
default=AuthAuthority.LOCAL,
description="Primary authentication authority",
frontend_type="select",
frontend_readonly=True,
frontend_required=False,
frontend_options=[
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}}
]
)
mandateId: Optional[str] = Field(
None,
description="ID of the mandate this user belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
# Register labels for User
register_model_labels(

View file

@ -284,15 +284,6 @@ class AppObjects:
result = []
for conn_dict in connections:
try:
# Convert string dates to datetime objects
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
if field in conn_dict and conn_dict[field]:
try:
if isinstance(conn_dict[field], str):
conn_dict[field] = datetime.fromisoformat(conn_dict[field].replace('Z', '+00:00'))
except (ValueError, TypeError):
conn_dict[field] = None
# Create UserConnection object
connection = UserConnection(
id=conn_dict["id"],
@ -831,8 +822,8 @@ class AppObjects:
logger.warning(f"No token found for connection: {connectionId}")
return None
# Sort by creation date and get the latest
tokens.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
# Sort by expiration date and get the latest (most recent expiration)
tokens.sort(key=lambda x: x.get("expiresAt", 0), reverse=True)
latest_token = Token(**tokens[0])
# Check if token is expired
@ -905,6 +896,35 @@ class AppObjects:
logger.error(f"Error deleting token for connection {connectionId}: {str(e)}")
raise
def cleanupExpiredTokens(self) -> int:
"""Clean up expired tokens for all connections, returns count of cleaned tokens"""
try:
from modules.shared.timezoneUtils import get_utc_timestamp
current_time = get_utc_timestamp()
cleaned_count = 0
# Get all tokens
all_tokens = self.db.getRecordset("tokens", recordFilter={})
for token_data in all_tokens:
if token_data.get("expiresAt") and token_data.get("expiresAt") < current_time:
# Token is expired, delete it
self.db.recordDelete("tokens", token_data["id"])
cleaned_count += 1
logger.debug(f"Cleaned up expired token {token_data['id']} for connection {token_data.get('connectionId')}")
# Clear cache to ensure fresh data
if cleaned_count > 0:
self._clearTableCache("tokens")
logger.info(f"Cleaned up {cleaned_count} expired tokens")
return cleaned_count
except Exception as e:
logger.error(f"Error cleaning up expired tokens: {str(e)}")
return 0
def logout(self) -> None:
"""Logout current user - clear user context and tokens"""
try:

View file

@ -465,17 +465,86 @@ register_model_labels(
class ChatWorkflow(BaseModel, ModelMixin):
"""Data model for a chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
status: str = Field(description="Current status of the workflow")
name: Optional[str] = Field(None, description="Name of the workflow")
currentRound: int = Field(description="Current round number")
lastActivity: float = Field(default_factory=get_utc_timestamp, description="Timestamp of last activity (UTC timestamp in seconds)")
startedAt: float = Field(default_factory=get_utc_timestamp, description="When the workflow started (UTC timestamp in seconds)")
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs")
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
tasks: List[TaskItem] = Field(default_factory=list, description="List of tasks in the workflow")
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this workflow belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
status: str = Field(
description="Current status of the workflow",
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
frontend_options=[
{"value": "running", "label": {"en": "Running", "fr": "En cours"}},
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
{"value": "stopped", "label": {"en": "Stopped", "fr": "Arrêté"}},
{"value": "error", "label": {"en": "Error", "fr": "Erreur"}}
]
)
name: Optional[str] = Field(
None,
description="Name of the workflow",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
currentRound: int = Field(
description="Current round number",
frontend_type="integer",
frontend_readonly=True,
frontend_required=False
)
lastActivity: float = Field(
default_factory=get_utc_timestamp,
description="Timestamp of last activity (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
startedAt: float = Field(
default_factory=get_utc_timestamp,
description="When the workflow started (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
logs: List[ChatLog] = Field(
default_factory=list,
description="Workflow logs",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
messages: List[ChatMessage] = Field(
default_factory=list,
description="Messages in the workflow",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
stats: Optional[ChatStat] = Field(
None,
description="Workflow statistics",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
tasks: List[TaskItem] = Field(
default_factory=list,
description="List of tasks in the workflow",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
# Register labels for ChatWorkflow
register_model_labels(

View file

@ -1335,7 +1335,7 @@ class ChatObjects:
retryCount=createdAction.get("retryCount", 0),
retryMax=createdAction.get("retryMax", 3),
processingTime=createdAction.get("processingTime"),
timestamp=datetime.fromtimestamp(float(createdAction.get("timestamp", get_utc_timestamp()))),
timestamp=float(createdAction.get("timestamp", get_utc_timestamp())),
result=createdAction.get("result"),
resultDocuments=createdAction.get("resultDocuments", [])
)

View file

@ -16,13 +16,50 @@ from modules.shared.timezoneUtils import get_utc_timestamp
class FileItem(BaseModel, ModelMixin):
"""Data model for a file item"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this file belongs to")
filename: str = Field(description="Name of the file")
mimeType: str = Field(description="MIME type of the file")
fileHash: str = Field(description="Hash of the file")
fileSize: int = Field(description="Size of the file in bytes")
creationDate: float = Field(default_factory=get_utc_timestamp, description="Date when the file was created (UTC timestamp in seconds)")
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this file belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
filename: str = Field(
description="Name of the file",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
mimeType: str = Field(
description="MIME type of the file",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
fileHash: str = Field(
description="Hash of the file",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
fileSize: int = Field(
description="Size of the file in bytes",
frontend_type="integer",
frontend_readonly=True,
frontend_required=False
)
creationDate: float = Field(
default_factory=get_utc_timestamp,
description="Date when the file was created (UTC timestamp in seconds)",
frontend_type="timestamp",
frontend_readonly=True,
frontend_required=False
)
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary"""
@ -94,10 +131,31 @@ register_model_labels(
class Prompt(BaseModel, ModelMixin):
"""Data model for a prompt"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Name of the prompt")
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
mandateId: str = Field(
description="ID of the mandate this prompt belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False
)
content: str = Field(
description="Content of the prompt",
frontend_type="textarea",
frontend_readonly=False,
frontend_required=True
)
name: str = Field(
description="Name of the prompt",
frontend_type="text",
frontend_readonly=False,
frontend_required=True
)
# Register labels for Prompt
register_model_labels(

View file

@ -512,7 +512,7 @@ async def refresh_token(
appInterface.deleteTokenByConnectionId(google_connection.id)
# Update the connection's expiration time
google_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt)
google_connection.expiresAt = float(refreshed_token.expiresAt)
google_connection.lastChecked = get_utc_timestamp()
google_connection.status = ConnectionStatus.ACTIVE

View file

@ -443,6 +443,31 @@ async def logout(
detail=f"Failed to logout: {str(e)}"
)
@router.post("/cleanup")
@limiter.limit("5/minute")
async def cleanup_expired_tokens(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Clean up expired tokens for the current user"""
try:
appInterface = getInterface(currentUser)
# Clean up expired tokens
cleaned_count = appInterface.cleanupExpiredTokens()
return {
"message": f"Cleanup completed successfully",
"tokens_cleaned": cleaned_count
}
except Exception as e:
logger.error(f"Error cleaning up expired tokens: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to cleanup expired tokens: {str(e)}"
)
@router.post("/refresh")
@limiter.limit("10/minute")
async def refresh_token(
@ -473,7 +498,8 @@ async def refresh_token(
logger.debug(f"Found Microsoft connection: {msft_connection.id}, status={msft_connection.status}")
# Get the token for this specific connection using the new method
current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=False)
# Enable auto-refresh to handle expired tokens gracefully
current_token = appInterface.getTokenForConnection(msft_connection.id, auto_refresh=True)
if not current_token:
raise HTTPException(
@ -494,7 +520,7 @@ async def refresh_token(
appInterface.deleteTokenByConnectionId(msft_connection.id)
# Update the connection's expiration time
msft_connection.expiresAt = datetime.fromtimestamp(refreshed_token.expiresAt)
msft_connection.expiresAt = float(refreshed_token.expiresAt)
msft_connection.lastChecked = get_utc_timestamp()
msft_connection.status = ConnectionStatus.ACTIVE

View file

@ -16,7 +16,7 @@ class ModelMixin:
"""
Convert a Pydantic model to a dictionary.
Handles both Pydantic v1 and v2.
Properly serializes datetime fields to ISO format strings.
All timestamp fields remain as float values.
Returns:
Dict[str, Any]: Dictionary representation of the model
@ -27,42 +27,10 @@ class ModelMixin:
else:
data: Dict[str, Any] = self.dict() # Pydantic v1
# Convert datetime fields to ISO format strings
for key, value in data.items():
if isinstance(value, datetime):
data[key] = value.isoformat()
elif isinstance(value, (int, float)) and self._is_timestamp_field(key):
# Handle timestamp fields based on field metadata
try:
data[key] = datetime.fromtimestamp(value).isoformat()
except (ValueError, TypeError):
# If conversion fails, keep the original value
pass
return data
def _is_timestamp_field(self, field_name: str) -> bool:
"""
Check if a field is a timestamp field based on field metadata.
Looks for 'UTC timestamp' in the field description.
"""
try:
# Get field info from Pydantic model
if hasattr(self, 'model_fields'):
# Pydantic v2
field_info = self.model_fields.get(field_name)
if field_info and field_info.description:
return 'UTC timestamp' in field_info.description
elif hasattr(self, '__fields__'):
# Pydantic v1
field_info = self.__fields__.get(field_name)
if field_info and field_info.field_info and field_info.field_info.description:
return 'UTC timestamp' in field_info.field_info.description
except Exception:
pass
# All fields (including timestamps) remain in their original format
# No conversions needed - timestamps are already float
# Fallback: return False for safety
return False
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
@ -89,6 +57,12 @@ class AttributeDefinition(BaseModel, ModelMixin):
options: Optional[List[Any]] = None
validation: Optional[Dict[str, Any]] = None
ui: Optional[Dict[str, Any]] = None
# New frontend metadata fields
readonly: bool = False
editable: bool = True
visible: bool = True
order: int = 0
placeholder: Optional[str] = None
# Global registry for model labels
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
@ -194,30 +168,92 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
if hasattr(modelClass, 'model_fields'): # Pydantic v2
fields = modelClass.model_fields
for name, field in fields.items():
# Extract frontend metadata from field info
field_info = field.field_info if hasattr(field, 'field_info') else None
# Check both direct attributes and extra field for frontend metadata
frontend_type = None
frontend_readonly = False
frontend_required = field.is_required()
frontend_options = None
if field_info:
# Try direct attributes first
frontend_type = getattr(field_info, 'frontend_type', None)
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
frontend_options = getattr(field_info, 'frontend_options', None)
# If not found, check extra field
if hasattr(field_info, 'extra') and field_info.extra:
if frontend_type is None:
frontend_type = field_info.extra.get('frontend_type')
if not frontend_readonly:
frontend_readonly = field_info.extra.get('frontend_readonly', False)
if frontend_required == field.is_required(): # Only override if we didn't get it from direct attribute
frontend_required = field_info.extra.get('frontend_required', frontend_required)
if frontend_options is None:
frontend_options = field_info.extra.get('frontend_options')
# Use frontend type if available, otherwise fall back to Python type
field_type = frontend_type if frontend_type else (field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation))
attributes.append({
"name": name,
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
"required": field.is_required() if hasattr(field, "is_required") else True,
"type": field_type,
"required": frontend_required,
"description": field.description if hasattr(field, "description") else "",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}",
"editable": True,
"editable": not frontend_readonly,
"visible": True,
"order": len(attributes)
"order": len(attributes),
"readonly": frontend_readonly,
"options": frontend_options
})
else: # Pydantic v1
fields = modelClass.__fields__
for name, field in fields.items():
# Extract frontend metadata from field info
field_info = field.field_info if hasattr(field, 'field_info') else None
# Check both direct attributes and extra field for frontend metadata
frontend_type = None
frontend_readonly = False
frontend_required = field.required
frontend_options = None
if field_info:
# Try direct attributes first
frontend_type = getattr(field_info, 'frontend_type', None)
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
frontend_options = getattr(field_info, 'frontend_options', None)
# If not found, check extra field
if hasattr(field_info, 'extra') and field_info.extra:
if frontend_type is None:
frontend_type = field_info.extra.get('frontend_type')
if not frontend_readonly:
frontend_readonly = field_info.extra.get('frontend_readonly', False)
if frontend_required == field.required: # Only override if we didn't get it from direct attribute
frontend_required = field_info.extra.get('frontend_required', frontend_required)
if frontend_options is None:
frontend_options = field_info.extra.get('frontend_options')
# Use frontend type if available, otherwise fall back to Python type
field_type = frontend_type if frontend_type else (field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_))
attributes.append({
"name": name,
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
"required": field.required,
"type": field_type,
"required": frontend_required,
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}",
"editable": True,
"editable": not frontend_readonly,
"visible": True,
"order": len(attributes)
"order": len(attributes),
"readonly": frontend_readonly,
"options": frontend_options
})
return {

View file

@ -92,6 +92,7 @@ git remote set-url origin https://valueon@github.com/valueonag/gateway
git remote set-url origin https://valueon@github.com/valueonag/frontend_agents
git remote set-url origin https://valueon@github.com/valueonag/wiki
git remote set-url origin https://valueon@github.com/valueonag/customer-svbe
git remote set-url origin https://valueon@github.com/valueonag/customer-althaus
### git delete workflow runs (cleanup)