feat: Add AlexHost deployment pipeline and bot control functionality
All checks were successful
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 59s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped

- Add Gitea Actions workflow for manual AlexHost deployment
- Add docker-compose.alexhost.yml for production deployment
- Add deploy-alexhost.sh script with server-side build support
- Add Bot Control feature (Start/Stop/Restart) for remote bot management
- Add discovery control endpoint in TeleBot
- Update TeleBot with StartPollingAsync/StopPolling/RestartPollingAsync
- Fix platform architecture issues by building on target server
- Update docker-compose configurations for all environments

Deployment tested successfully:
- TeleShop: healthy at https://teleshop.silentmary.mywire.org
- TeleBot: healthy with discovery integration
- SilverPay: connectivity verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 12:33:46 +00:00
parent 0997cc8c57
commit 86f19ba044
20 changed files with 1186 additions and 37 deletions

View File

@@ -793,5 +793,131 @@ public class BotsController : Controller
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
}
// POST: Admin/Bots/StartBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StartBot(Guid id)
{
_logger.LogInformation("Start bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "start");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start bot {BotId}", id);
TempData["Error"] = $"Failed to start bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
// POST: Admin/Bots/StopBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StopBot(Guid id)
{
_logger.LogInformation("Stop bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "stop");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop bot {BotId}", id);
TempData["Error"] = $"Failed to stop bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
// POST: Admin/Bots/RestartBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RestartBot(Guid id)
{
_logger.LogInformation("Restart bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "restart");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to restart bot {BotId}", id);
TempData["Error"] = $"Failed to restart bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
#endregion
}

View File

@@ -58,9 +58,14 @@ public class ProductsController : Controller
if (!ModelState.IsValid)
{
var categories = await _categoryService.GetAllCategoriesAsync();
ViewData["Categories"] = categories.Where(c => c.IsActive);
// FIX: Re-populate VariantCollections for view rendering when validation fails
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
return View(model);
}
@@ -134,6 +139,11 @@ public class ProductsController : Controller
{
var categories = await _categoryService.GetAllCategoriesAsync();
ViewData["Categories"] = categories.Where(c => c.IsActive);
// FIX: Re-populate VariantCollections for view rendering when validation fails
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
ViewData["ProductId"] = id;
return View(model);
}

View File

@@ -180,7 +180,7 @@
<hr />
<div class="d-flex gap-2">
<div class="d-flex gap-2 flex-wrap">
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-info">
@@ -195,6 +195,37 @@
</a>
}
</div>
@if (Model.DiscoveryStatus == "Configured")
{
<hr />
<h6 class="mb-2"><i class="fas fa-sliders-h"></i> Bot Control</h6>
<div class="d-flex gap-2 flex-wrap">
<form asp-area="Admin" asp-controller="Bots" asp-action="StartBot" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-success">
<i class="fas fa-play"></i> Start
</button>
</form>
<form asp-area="Admin" asp-controller="Bots" asp-action="StopBot" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to stop the bot? Users will not be able to interact with it.')">
<i class="fas fa-stop"></i> Stop
</button>
</form>
<form asp-area="Admin" asp-controller="Bots" asp-action="RestartBot" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-warning">
<i class="fas fa-redo"></i> Restart
</button>
</form>
</div>
<small class="text-muted mt-2 d-block">
<i class="fas fa-info-circle"></i> Controls the Telegram polling connection on the remote bot instance.
</small>
}
</div>
</div>
}
@@ -331,7 +362,8 @@
<hr />
<form action="/Admin/Bots/Delete/@Model.Id" method="post">
<button type="submit" class="btn btn-danger w-100"
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-danger w-100"
onclick="return confirm('Are you sure you want to delete this bot? This action cannot be undone.')">
<i class="bi bi-trash"></i> Delete Bot
</button>

View File

@@ -37,15 +37,17 @@
The TeleBot must be running and configured with the same discovery secret.
</p>
<form asp-area="Admin" asp-controller="Bots" asp-action="ProbeRemote" method="post">
<form action="/Admin/Bots/ProbeRemote" method="post">
@Html.AntiForgeryToken()
<div class="row mb-3">
<div class="col-md-8">
<label for="IpAddress" class="form-label">IP Address / Hostname</label>
<input name="IpAddress" id="IpAddress" value="@Model.IpAddress" class="form-control"
placeholder="e.g., 192.168.1.100 or telebot.example.com" required />
<small class="text-muted">The IP address or hostname where TeleBot is running</small>
placeholder="e.g., telebot, 193.233.245.41, or telebot.example.com" required />
<small class="text-muted">
Use <code>telebot</code> for same-server Docker deployments, or the public IP/hostname for remote servers
</small>
</div>
<div class="col-md-4">
<label for="Port" class="form-label">Port</label>
@@ -107,7 +109,7 @@
<h5 class="mb-0"><i class="fas fa-robot"></i> Step 2: Register Bot</h5>
</div>
<div class="card-body">
<form asp-area="Admin" asp-controller="Bots" asp-action="RegisterRemote" method="post">
<form action="/Admin/Bots/RegisterRemote" method="post">
@Html.AntiForgeryToken()
<!-- Hidden fields to preserve discovery data -->
@@ -188,7 +190,7 @@
Now enter the Telegram bot token from BotFather to activate this bot.
</p>
<form asp-area="Admin" asp-controller="Bots" asp-action="ConfigureRemote" method="post">
<form action="/Admin/Bots/ConfigureRemote" method="post">
@Html.AntiForgeryToken()
<!-- Hidden fields -->