diff --git a/src/central/gui/form_descriptors.py b/src/central/gui/form_descriptors.py index dd8a561..1a420c2 100644 --- a/src/central/gui/form_descriptors.py +++ b/src/central/gui/form_descriptors.py @@ -87,13 +87,26 @@ def _type_to_widget_and_options(field_name: str, field_type: type) -> tuple[str, if inner_type is str: return "csv", None + # list[int] -> csv_int (v0.11.3). Mirrors csv but the POST parser + # coerces each comma-separated token through int(); non-numeric + # tokens are dropped with a warning log. Added when + # celestrak_tle's extra_norad_ids: list[int] triggered the + # adapter-edit form to 500 on production. + if inner_type is int: + return "csv_int", None + # list[] -> repeatable per-row editor (sub-columns resolved # by describe_fields, which recurses into the row model). if isinstance(inner_type, type) and issubclass(inner_type, BaseModel): return "model_list", None + inner_name = ( + inner_type.__name__ if isinstance(inner_type, type) else repr(inner_type) + ) if inner_type is not None else "?" raise NotImplementedError( - f"Field '{field_name}' has unsupported list type: list[{inner_type.__name__ if inner_type else '?'}]" + f"Field '{field_name}' has unsupported list type: list[{inner_name}]. " + f"Supported inner types: str (csv), int (csv_int), Literal[...] (checkboxes), " + f"BaseModel subclasses (model_list)." ) # dict -> json textarea (generic; e.g. EnrichmentConfig.backend_settings). diff --git a/src/central/gui/routes.py b/src/central/gui/routes.py index af51b52..3b784b7 100644 --- a/src/central/gui/routes.py +++ b/src/central/gui/routes.py @@ -946,6 +946,26 @@ async def setup_adapters_submit(request: Request) -> Response: else: new_settings[field.name] = [] + elif field.widget == "csv_int": + # v0.11.3: list[int] support. Coerce per-token; drop non-numeric + # entries with a warning so the operator can spot typos in the log + # rather than getting a 500. + value = form.get(form_key, "").strip() + parsed: list[int] = [] + if value: + for tok in value.split(","): + tok = tok.strip() + if not tok: + continue + try: + parsed.append(int(tok)) + except ValueError: + logger.warning( + "csv_int: dropped non-numeric token", + extra={"field": field.name, "token": tok}, + ) + new_settings[field.name] = parsed + elif field.widget == "select": value = form.get(form_key, "").strip() if value and field.options and value not in field.options: @@ -1715,6 +1735,23 @@ async def adapters_edit_submit( parsed_values[field.name] = [v.strip() for v in raw.split(",") if v.strip()] else: parsed_values[field.name] = [] + elif field.widget == "csv_int": + # v0.11.3: parallel to "csv" but coerces each token through + # int(), dropping non-numeric entries with a warning. + parsed_ints: list[int] = [] + if raw.strip(): + for tok in raw.split(","): + tok = tok.strip() + if not tok: + continue + try: + parsed_ints.append(int(tok)) + except ValueError: + logger.warning( + "csv_int: dropped non-numeric token", + extra={"field": field.name, "token": tok}, + ) + parsed_values[field.name] = parsed_ints elif field.widget == "select": value = raw.strip() if raw else None if value and field.options and value not in field.options: diff --git a/src/central/gui/templates/adapters_edit.html b/src/central/gui/templates/adapters_edit.html index 977d069..6fded2d 100644 --- a/src/central/gui/templates/adapters_edit.html +++ b/src/central/gui/templates/adapters_edit.html @@ -104,6 +104,16 @@ {{ errors[field.name] }} {% endif %} + {% elif field.widget == "csv_int" %} + + + Comma-separated integers{% if field.description %} — {{ field.description }}{% endif %} + {% if errors and errors[field.name] %} + {{ errors[field.name] }} + {% endif %} + {% elif field.widget == "select" %}