feat: Add AlexHost deployment pipeline and bot control functionality
- 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:
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user